ShellPilot
0.4.0-preview0008
GitHub Copilot in your PowerShell terminal: device-flow auth, model listing, chat and agentic tool-calling with usage and cost.
Minimum PowerShell version
7.0
Installation Options
Owners
Copyright
(c) raandree. All rights reserved.
Package Details
Author(s)
- raandree
Tags
GitHubCopilot Copilot AI LLM Chat Agent
Functions
Clear-ShpChat Clear-ShpContext Clear-ShpToolPolicy Clear-ShpUsage Compress-ShpChat ConvertTo-ShpTokenCount Get-ShpChat Get-ShpContext Get-ShpCosineSimilarity Get-ShpCostEstimate Get-ShpDefault Get-ShpMcpServer Get-ShpModel Get-ShpModelName Get-ShpTool Get-ShpToolPolicy Get-ShpUsage Initialize-Shp Invoke-Shp Invoke-ShpBatch Register-ShpMcpServer Register-ShpTool Request-ShpEmbedding Resolve-ShpError Select-ShpModel Set-ShpContext Set-ShpToolPolicy Start-ShpChat Unregister-ShpMcpServer Unregister-ShpTool
PSEditions
Dependencies
This module has no dependencies.
Release Notes
## [0.4.0-preview0008] - 2026-08-19
### Added
- `Register-ShpMcpServer`, `Get-ShpMcpServer` and `Unregister-ShpMcpServer`
attach MCP (Model Context Protocol) servers so their tools are offered to the
model alongside the built-ins and any registered user tools. Opt out for one
call with `Invoke-Shp -DisableMcp`; nothing is offered until you attach a
server, so the default posture is unchanged.
**Both protocol eras are supported over stdio.** Verified from the
specification rather than from memory, which changed the design: the current
revision **2026-07-28** removed the `initialize` handshake entirely - a modern
request is stateless and carries its protocol version and client capabilities
in `_meta`, with `server/discover` as a mandatory RPC - while nearly every
server in the field still expects the handshake. The client therefore probes
with `server/discover` and falls back to `initialize` on any other error or a
timeout, never keyed to one error code. Both eras were negotiated live, and
the real Azure MCP Server 2.0.5 turned out to be a **legacy** server - which
settles whether supporting both was worth it.
See [specs/021-mcp-server-support.md](specs/021-mcp-server-support.md).
- Tool names are namespaced `mcp_<alias>_<tool>` using the alias **you** chose,
not the server's self-reported name, which the protocol says nothing
verifies. The sanitiser is written to a measured constraint, not a guessed
one: the Copilot endpoint enforces `^[a-zA-Z0-9_-]{1,128}$`, so a dot - which
MCP explicitly permits in a tool name - becomes `_`. This matters because a
rejected name identifies the offending tool only by its **index** in the
request, and the resulting 400 is masked by the chat-to-responses fallback,
surfacing as "model ... does not support Responses API" - a true statement
about an entirely different problem.
- `Get-ShpTool` now lists MCP tools next to user tools, with a new `Origin`
column (`User` or `Mcp`) and the contributing `Server`. `Invoke-Shp` reports
`McpEnabled`, `McpServersAvailable`, `McpToolsAvailable` and `McpToolsCalled`,
and an MCP call emits the same `ToolCall` progress record as every other tool,
so a host renders it identically.
### Security
- **An MCP server is a third-party process running with your privileges, and
there is no sandbox.** Its tool names and descriptions are untrusted input the
model reads on every round-trip, and its results are untrusted content. Three
controls follow, each stated with its limits:
- **Nothing is discovered.** No scan of the working directory, `.vscode` or a
user profile. `Register-ShpMcpServer -Path` reads a file you name. A
configuration file is a command line, and a command line is arbitrary code.
- **The tool list is frozen at registration.** The client opens no
`subscriptions/listen` stream, so it receives no
`notifications/tools/list_changed` and a server cannot add or alter tools
after you approved them. Refreshing is an explicit `-Force`. This does not
stop a server changing its *behaviour*.
- **The child environment is built, not inherited.** Unlike `run_command`,
which deliberately inherits the whole block for compatibility, an MCP child
starts from a minimal base plus exactly the variables you name in
`-Environment`, so an ambient `$env:` credential is not handed to somebody
else's process.
- **`Set-ShpToolPolicy` cannot gate an MCP call, and this is stated rather than
implied.** Its rules match resolved filesystem paths and leading command
tokens; a tool call has neither. Demonstrated in one live Turn under
`Read(<repo>/**)`: the built-in `read_file` was denied with a reason and the
MCP tool call ran. A policy that scopes `read_file` to one directory does
nothing about an attached filesystem server. Reduce reach at attachment
instead, with `-ToolName`.
- The injection path was measured, not asserted. With a hostile instruction in a
tool *description* only, the model read a decoy credentials file and passed
its contents to the third-party server as a tool argument - the server's own
log confirms receipt. The same run with `-DisableFileAccess` read nothing and
leaked nothing.
- A configuration entry asking for `sandboxEnabled` **warns and still starts**:
a configuration written for a sandboxing host is exactly the one you want to
reuse. The gap is surfaced twice rather than being fatal once - a warning that
says the server is starting unsandboxed, plus `SandboxRequested` on the server
record, because a warning scrolls away and a property does not. An entry
carrying an unresolved `${...}` variable is refused, because starting the
literal text would run a different command than the file describes.
- `Get-ShpMcpServer` reports environment variable **names** only, never values.
### Known limits
- stdio transport only. Streamable HTTP is deferred with the MCP Authorization
framework; a half-authorised HTTP client would be worse than none.
- `Invoke-ShpBatch` does not use attached servers and warns once. A worker
runspace inherits nothing, so replaying an attachment would start one copy of
every server per worker.
- A server that exits is marked `Faulted` rather than restarted automatically.
The specification says a client SHOULD restart one; automatic respawn of
third-party code inside an unattended loop turns one crash into a crash loop
nobody is watching. Re-attach with `Register-ShpMcpServer -Force`.
- Resources, prompts, sampling, elicitation and roots are out of scope for this
version. Client capabilities are declared empty, so a modern server needing
one gets a protocol error naming it rather than hanging.
- The cached OAuth token is now protected at rest, closing open decision #5.
On Windows it is DPAPI-encrypted for your account; on every platform the file
is restricted to the current user. Measured before the change, the real file
was 40 bytes of clear text with the profile's inherited ACL, readable by
`BUILTIN\Administrators` as well as by you.
The file is self-describing - `SHPv1:DPAPI:...` or `SHPv1:NONE:...` - and
`Initialize-Shp` reports which protection it applied, because a scheme that
silently degrades to clear text is worse than clear text. On Linux and macOS
there is no DPAPI equivalent without a dependency, so the scheme is `NONE`
and file permissions (mode 600) are the only control; that is stated in the
file and on screen rather than implied.
**No runtime dependency was added** - `ConvertTo-SecureString` is built in.
SecretManagement was considered and rejected: SecretStore prompts to unlock,
which would break every unattended run, and configuring it not to prompt
reduces it to file permissions while costing this module its empty dependency
list. This buys protection against another principal on the machine; it does
**not** protect against code running as you, and no candidate scheme would.
See [specs/020-encrypted-token-storage.md](specs/020-encrypted-token-storage.md).
- An existing clear-text token file keeps working and is **upgraded in place by
`Initialize-Shp` without re-authenticating**, so nobody needs a browser just
to gain protection. A protected file that cannot be decrypted - typically one
copied from another machine or account - throws with an actionable message
naming `Initialize-Shp -Force`, rather than sending a ciphertext to the
service as a bearer token.
- `Set-ShpToolPolicy`, `Get-ShpToolPolicy` and `Clear-ShpToolPolicy` scope what
the unsandboxed file and shell tools may reach, so an unattended run can be
given the access it needs instead of the caller's entire filesystem and shell.
Rules are written `Read(./src/**)`, `Write(./out/**)`, `Shell(git status)`,
with a leading `!` to deny; any matching deny beats every matching allow.
**Nothing changes until you set a policy** - and once you do, the model is
denied by default. Refusals are reported on the result as `ToolCallsDenied`,
and the policy travels into every `Invoke-ShpBatch` worker, because a worker
inherits no module state and would otherwise be the one unguarded path.
This closes a real gap rather than a symmetry: `-Confirm` is interactive only
so an unattended run never prompts, and `-DisableFileAccess` /
`-DisableTerminal` are all-or-nothing, so the only safe unattended setting was
"all tools off".
See [specs/019-tool-access-policy.md](specs/019-tool-access-policy.md).
- Paths are matched on the absolute, **link-resolved** path rather than the
string the model supplied, so neither a `..` segment nor a directory junction
walks out of an allowed root, and patterns are anchored so a rule for `out`
cannot match `outsider`. `run_command` is matched on whole leading tokens and
**refuses any shell metacharacter** whatever the rules say, because
`git status; curl ...` would otherwise pass a rule that only ever meant
`git status`. A `Shell` rule constrains which program runs, not what it does;
the spec states that limit and the others plainly.
- `Invoke-Shp -RetryDelaySec`, and `-TimeoutSec`, `-MaxRetryCount`,
`-RetryDelaySec` and `-NetworkOutageToleranceSec` on `Get-ShpModel` and
`Request-ShpEmbedding`. All four now resolve identically everywhere - explicit
parameter, then `Set-ShpContext`, then the built-in default - and apply to
**every** request the module makes, including the OAuth-to-session token
exchange, `/models` and embeddings. Those three previously called the retry
wrapper with the module's own defaults, so `Set-ShpContext -TimeoutSec 10
-MaxRetryCount 0` silently did not apply to them; and `Invoke-Shp` resolved
its options *after* the token exchange had already run, so an explicit
`-TimeoutSec` never reached the one request that gates every other one. There
is deliberately no exemption for the auth handshake: the exchange is cached,
so `-MaxRetryCount 0` costs at most one un-retried att
FileList
- ShellPilot.nuspec
- ShellPilot.psd1
- en-US\about_ShellPilot.help.txt
- data\PriceTable.psd1
- ShellPilot.Format.ps1xml
- ShellPilot.psm1
- en-US\ShellPilot-help.xml
Version History
| Version | Downloads | Last updated |
|---|---|---|
| 0.4.0-previe... (current version) | 3 | 8/19/2026 |
| 0.4.0-previe... | 5 | 8/12/2026 |
| 0.4.0-previe... | 7 | 8/12/2026 |
| 0.4.0-previe... | 5 | 8/11/2026 |
| 0.4.0-previe... | 3 | 8/11/2026 |
| 0.4.0-previe... | 10 | 8/6/2026 |
| 0.4.0-previe... | 10 | 7/28/2026 |
| 0.4.0-previe... | 3 | 7/28/2026 |
| 0.3.1 | 54 | 7/23/2026 |
| 0.3.1-previe... | 3 | 7/23/2026 |
| 0.3.0-previe... | 8 | 7/12/2026 |
| 0.3.0-previe... | 7 | 7/9/2026 |
| 0.3.0-previe... | 4 | 7/9/2026 |
| 0.3.0-previe... | 7 | 7/9/2026 |
| 0.2.1-previe... | 8 | 7/8/2026 |
| 0.2.0 | 37 | 7/8/2026 |
| 0.2.0-previe... | 6 | 7/8/2026 |
| 0.2.0-previe... | 4 | 7/8/2026 |
| 0.2.0-previe... | 31 | 6/12/2026 |