@sigrun/ext-mcp 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +75 -6
- package/dist/cli.js +6 -2
- package/dist/client.d.ts +35 -31
- package/dist/client.js +639 -136
- package/dist/config.d.ts +45 -9
- package/dist/config.js +111 -15
- package/dist/extension.d.ts +26 -45
- package/dist/extension.js +261 -136
- package/dist/index.d.ts +3 -4
- package/dist/index.js +3 -5
- package/dist/oauth.d.ts +15 -0
- package/dist/oauth.js +50 -0
- package/dist/spawn-env.d.ts +23 -0
- package/dist/spawn-env.js +48 -0
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.js +4 -0
- package/dist/user-servers.d.ts +93 -0
- package/dist/user-servers.js +422 -0
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,85 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
|
+
### Security
|
|
10
|
+
- **stdio MCP servers receive an allowlisted environment instead of sigrun's
|
|
11
|
+
process environment.** `SIGRUN_API_TOKEN` and provider keys were inherited
|
|
12
|
+
by every spawned stdio server; they now see only the variables their
|
|
13
|
+
declaration names plus a minimal base. Previously published `0.2.0` is
|
|
14
|
+
affected; `0.2.1` is the first release with the fix.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **Upgrade behavior:** catalog servers now default to `credentials: "user"`.
|
|
18
|
+
Existing installs that rely on one system-wide server must explicitly
|
|
19
|
+
select `credentials: "shared"` and provide `sharedAuthority`. Shared MCP
|
|
20
|
+
servers hand every permitted user's agent the admin's credential for that
|
|
21
|
+
service.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Per-user tool publication is declared even with an empty or shared-only
|
|
25
|
+
catalog, so lifecycle activation and prompt rendering no longer fail when
|
|
26
|
+
there are no personal catalog servers. Because the declaration is
|
|
27
|
+
unconditional, core's boot sweep now writes `sigrun.mcp@<owner>` into every
|
|
28
|
+
owned conversation's selection on the first boot after upgrading, even with
|
|
29
|
+
no `mcp.json` or `userServers: "none"`. That is a one-time durable write:
|
|
30
|
+
the name resolves to nothing while no tools are published, so nothing
|
|
31
|
+
changes for the conversation, and later boots report zero.
|
|
32
|
+
- Catalog enablement now mounts through `surfaces.settings` and uses core's
|
|
33
|
+
actual flat settings shape: each toggle and changed key is the bare server
|
|
34
|
+
name. Shared-authority help is punctuated and guarded against missing
|
|
35
|
+
programmatic input.
|
|
36
|
+
- Prompt sections resolve the conversation owner through the harness's
|
|
37
|
+
committed-read surface instead of the server-bound identity bridge, so
|
|
38
|
+
harness-only boots cannot hang while preparing a request.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- User-supplied MCP servers in per-user settings, gated by `userServers:
|
|
42
|
+
"none" | "remote" | "all"` and validated with the operator config parser.
|
|
43
|
+
Tools publish as `u_<server>/<tool>`; live policy changes reconcile every
|
|
44
|
+
active user. The `all` mode also permits URL servers, but reports
|
|
45
|
+
user-supplied stdio as a named configuration error because the host's
|
|
46
|
+
static user-service contract cannot launch it safely.
|
|
47
|
+
- Per-user OAuth start/complete extension routes with ten-minute in-memory
|
|
48
|
+
PKCE state scoped by user and server. Completion stores the raw access
|
|
49
|
+
token in the caller's scoped secrets; the CLI refuses per-user servers.
|
|
50
|
+
- Token storage retires and reconnects the caller's catalog connection.
|
|
51
|
+
Core owns the process lifecycle: a still-live child retains its original
|
|
52
|
+
environment until idle-stop or exit because the host contract cannot force
|
|
53
|
+
a relaunch.
|
|
54
|
+
- MCP catalog entries default to per-user credentials, with lazy per-user
|
|
55
|
+
stdio services, scoped token injection, remote connections, and tools
|
|
56
|
+
published only for their owner. Opt-in availability and per-user settings
|
|
57
|
+
control enablement; disabling removes tools and drains active calls.
|
|
58
|
+
- Shared entries require `sharedAuthority`, retain system-tool behavior,
|
|
59
|
+
and enforce their optional user allowlist and enablement at call time.
|
|
60
|
+
Prompt sections list only servers available to the calling user.
|
|
61
|
+
|
|
62
|
+
### Security
|
|
63
|
+
- Stdio servers no longer inherit sigrun's environment. They are spawned
|
|
64
|
+
with an allowlist (`PATH`, `HOME`, `TMPDIR`, `LANG`, `LC_*`, `TZ`,
|
|
65
|
+
`TERM`, `USER`, `LOGNAME`, `SHELL`, `NPM_CONFIG_CACHE`,
|
|
66
|
+
`SSH_AUTH_SOCK`) plus their configured `env` and stored token.
|
|
67
|
+
Previously every server could read `SIGRUN_API_TOKEN`,
|
|
68
|
+
`SIGRUN_PROXY_SECRET` and provider keys. A server that relied on an
|
|
69
|
+
inherited variable must now list it in its `env`.
|
|
70
|
+
|
|
71
|
+
## [0.2.0] - 2026-10-22
|
|
72
|
+
### Added
|
|
73
|
+
- Remote MCP servers: a `servers` entry may be `{url, headers?, exposure?,
|
|
74
|
+
toolExposure?, timeout?}` instead of a stdio `{command, args}`. Streamable
|
|
75
|
+
HTTP is the transport (POST per frame, JSON or SSE reply,
|
|
76
|
+
`mcp-session-id` captured and echoed); a legacy HTTP+SSE server is adopted
|
|
77
|
+
automatically — whether it 404/405s the POST or (crawl4ai's shape) answers
|
|
78
|
+
it with an `endpoint` event — so no supergateway-style adapter is needed.
|
|
79
|
+
- `mcp.json` now resolves from the sigrun config dir
|
|
80
|
+
(`SIGRUN_CONFIG_DIR/mcp.json`) before the home fallback, matching every
|
|
81
|
+
other sigrun store.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
- SSE parsing handles CRLF line endings (the spec allows them; live servers
|
|
85
|
+
emit them).
|
|
86
|
+
- Streamable replies stop at the id-matching frame — servers legitimately
|
|
87
|
+
hold SSE streams open past the reply.
|
|
9
88
|
|
|
10
89
|
## [0.1.0] - 2026-10-13
|
|
11
90
|
### Added
|
package/README.md
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Register MCP server tools as pi-durable tools, named `<server>/<tool>`.
|
|
4
4
|
|
|
5
|
-
Config:
|
|
6
|
-
|
|
5
|
+
Config: the sigrun config dir's `mcp.json` (`SIGRUN_CONFIG_DIR/mcp.json`; a
|
|
6
|
+
bare user install falls back to `~/.config/sigrun/mcp.json`; the
|
|
7
|
+
`SIGRUN_MCP_CONFIG` env override points at any path). Each entry under
|
|
8
|
+
`"servers"` names an MCP server — stdio or remote:
|
|
7
9
|
|
|
8
10
|
```json
|
|
9
11
|
{
|
|
@@ -13,6 +15,11 @@ pointing at any path). Each entry under `"servers"` names a stdio MCP server:
|
|
|
13
15
|
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
14
16
|
"exposure": "codemode",
|
|
15
17
|
"timeout": 120
|
|
18
|
+
},
|
|
19
|
+
"search": {
|
|
20
|
+
"url": "https://mcp.example.test/mcp",
|
|
21
|
+
"headers": { "Authorization": "Bearer <token>" },
|
|
22
|
+
"exposure": "direct"
|
|
16
23
|
}
|
|
17
24
|
}
|
|
18
25
|
}
|
|
@@ -20,8 +27,31 @@ pointing at any path). Each entry under `"servers"` names a stdio MCP server:
|
|
|
20
27
|
|
|
21
28
|
## Server fields
|
|
22
29
|
|
|
23
|
-
- `command`, `args` (
|
|
24
|
-
- `
|
|
30
|
+
- `command`, `args` (stdio) — how to spawn the server.
|
|
31
|
+
- `url` (remote) — the server endpoint (http/https). Streamable HTTP is the
|
|
32
|
+
transport; a legacy HTTP+SSE server (2024-11-05 shape — GET stream +
|
|
33
|
+
`endpoint` event + message POSTs) is adopted automatically, so no adapter
|
|
34
|
+
process is needed.
|
|
35
|
+
- `headers` (remote) — extra request headers (auth etc.), sent on every
|
|
36
|
+
request. For per-user servers, configured `Authorization` is removed;
|
|
37
|
+
only that user's scoped token supplies it.
|
|
38
|
+
- `env` (stdio) — extra environment variables for the child process. For
|
|
39
|
+
per-user servers, configured `MCP_ACCESS_TOKEN` is removed; only that
|
|
40
|
+
user's scoped token supplies it.
|
|
41
|
+
- `credentials` — `"user"` (default) or `"shared"`. User-credential servers
|
|
42
|
+
start per user in the host's service jail; remote user servers connect
|
|
43
|
+
with the user's scoped token. Existing installs needing one system-wide
|
|
44
|
+
server must explicitly select `"shared"`.
|
|
45
|
+
- `sharedAuthority` — required, non-empty one-line reason when
|
|
46
|
+
`credentials` is `"shared"`. Shared MCP servers hand every permitted
|
|
47
|
+
user's agent the admin's credential for that service; the settings
|
|
48
|
+
section displays this reason and warning beside the server.
|
|
49
|
+
- `users` — optional string allowlist for a shared server. Only those
|
|
50
|
+
users may call its tools; this field is invalid for user-credential
|
|
51
|
+
servers. Ownerless calls fail closed.
|
|
52
|
+
- `availability` — `"all"` (default) or `"opt-in"`. The per-user settings
|
|
53
|
+
section stores each server's enablement as a flat boolean keyed by its
|
|
54
|
+
bare server name. `"all"` starts enabled; `"opt-in"` starts disabled.
|
|
25
55
|
- `enabled` (default `true`) / `disabled` — skip the server entirely. Set one,
|
|
26
56
|
never both: a server that is simultaneously on and off is a config error and
|
|
27
57
|
fails loudly.
|
|
@@ -33,8 +63,47 @@ pointing at any path). Each entry under `"servers"` names a stdio MCP server:
|
|
|
33
63
|
- `toolExposure` — per-tool overrides of `exposure`: an object mapping tool
|
|
34
64
|
names (exact) or `*`-globs (e.g. `"get-*"`) to exposure words. An exact name
|
|
35
65
|
always wins; among matching patterns the FIRST one in config order wins.
|
|
36
|
-
- `oauth` — the authorization-code (PKCE) flow block
|
|
37
|
-
<server>`
|
|
66
|
+
- `oauth` (stdio) — the authorization-code (PKCE) flow block. Shared servers
|
|
67
|
+
use `sigrun-ext-mcp oauth <server>` and the file token store. Per-user
|
|
68
|
+
servers use `POST /v1/ext/sigrun-mcp/oauth/<server>/start`, then
|
|
69
|
+
`POST /v1/ext/sigrun-mcp/oauth/<server>/complete` with
|
|
70
|
+
`{ "redirectedUrl": "<full redirected URL>" }`; the CLI refuses these
|
|
71
|
+
servers so their credentials cannot enter the shared store.
|
|
72
|
+
|
|
73
|
+
Per-user PKCE state stays in memory for ten minutes and is scoped to the
|
|
74
|
+
authenticated user and server. Completion stores only the raw access token in
|
|
75
|
+
that user's `token:<server>` secret. It then retires and reconnects that
|
|
76
|
+
user's catalog connection. Core owns the process lifecycle: releasing the last
|
|
77
|
+
handle arms idle-stop, so a still-live child keeps its original environment
|
|
78
|
+
until that timer fires or it crashes; the extension cannot force a relaunch
|
|
79
|
+
through the host contract. An already idle-stopped server launches fresh on
|
|
80
|
+
the next activation and reads the new token immediately.
|
|
81
|
+
|
|
82
|
+
`userServers` is the separate admin policy for user-supplied servers:
|
|
83
|
+
`"none"` (default), `"remote"`, or `"all"`. It is distinct from the admin
|
|
84
|
+
catalog's `credentials` and `availability` fields above. Catalog names
|
|
85
|
+
starting `u_` are reserved for those user-supplied servers and rejected.
|
|
86
|
+
|
|
87
|
+
When enabled, the **Your MCP servers** settings section accepts a JSON object
|
|
88
|
+
under the `servers` key. Entries use the same validation rules as `mcp.json`;
|
|
89
|
+
a user server name may not start with `u_`. Its tools publish only to that
|
|
90
|
+
user as `u_<server>/<tool>`. `"remote"` permits URL servers and ignores stdio;
|
|
91
|
+
`"all"` permits URL servers too. A live policy change reconciles every active
|
|
92
|
+
user, retiring disallowed connections and removing their tools without a
|
|
93
|
+
restart. Lifecycle events re-read the policy from `mcp.json`, since this
|
|
94
|
+
operator file is not part of core's settings document. The contributed
|
|
95
|
+
settings sections are selected at extension load: changing `none` to an
|
|
96
|
+
enabled policy needs a restart to add the editor, and changing to `none`
|
|
97
|
+
needs a restart to remove it; connection gating itself takes effect at the
|
|
98
|
+
next lifecycle event. A catalog server literally named `servers` is rejected
|
|
99
|
+
because it would shadow this settings namespace.
|
|
100
|
+
|
|
101
|
+
**Named limitation — `all` and user-supplied stdio:** sigrun declares per-user
|
|
102
|
+
services statically at load and passes no server name to `UserService.start`.
|
|
103
|
+
Consequently a dynamic, named user-supplied stdio service cannot be launched.
|
|
104
|
+
Under `"all"`, such an entry is reported as a configuration error naming the
|
|
105
|
+
server; its command is never spawned in sigrun's process, and no tools are
|
|
106
|
+
published. `"all"` does **not** make user-supplied stdio functional.
|
|
38
107
|
|
|
39
108
|
## Exposure: how tools reach callers
|
|
40
109
|
|
package/dist/cli.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* flow / 5-minute timeout).
|
|
18
18
|
*/
|
|
19
19
|
import { createInterface } from "node:readline";
|
|
20
|
-
import { loadMcpConfig, MCP_CONFIG_ENV } from "./config.js";
|
|
20
|
+
import { isStdioServer, loadMcpConfig, MCP_CONFIG_ENV, } from "./config.js";
|
|
21
21
|
import { startOauth } from "./oauth.js";
|
|
22
22
|
import { resolveMcpTokenPath } from "./tokens.js";
|
|
23
23
|
/** The flow timeout (matches startOauth's default; explicit so the user's
|
|
@@ -71,7 +71,11 @@ export async function runCli(argv, io = { stdout: process.stdout, stderr: proces
|
|
|
71
71
|
io.stderr.write(`error: no server named "${parsed.server}" in the MCP config\n`);
|
|
72
72
|
return 1;
|
|
73
73
|
}
|
|
74
|
-
if (server.
|
|
74
|
+
if (server.credentials !== "shared") {
|
|
75
|
+
io.stderr.write(`error: server "${parsed.server}" uses per-user credentials; use POST /v1/ext/sigrun-mcp/oauth/${parsed.server}/start instead\n`);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
if (!isStdioServer(server) || server.oauth === undefined) {
|
|
75
79
|
io.stderr.write(`error: server "${parsed.server}" has no oauth block in the MCP config\n`);
|
|
76
80
|
return 1;
|
|
77
81
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* The MCP client: a transport-neutral JSON-RPC 2.0 engine plus two
|
|
3
|
+
* transports.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* stdio (the original transport): newline-delimited JSON-RPC over a spawned
|
|
6
|
+
* child's pipes. Remote (streamable HTTP): every request POSTs to the
|
|
7
|
+
* configured URL; the reply is a JSON body OR an SSE stream. A server that
|
|
8
|
+
* 404/405s the POST (the pre-2025-03-26 "HTTP+SSE" transport — still common,
|
|
9
|
+
* e.g. many self-hosted servers) falls back to the legacy shape: a GET
|
|
10
|
+
* stream for server→client frames and a discovered message endpoint for
|
|
11
|
+
* client→server POSTs.
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle (both transports): `initialize` handshake (protocolVersion
|
|
14
|
+
* "2025-06-18", clientInfo, capabilities) → `notifications/initialized` →
|
|
15
|
+
* `tools/list` (discovery) → `tools/call` (execution). Requests correlate
|
|
16
|
+
* by id; the response frame's `id` must match a pending request (a
|
|
17
|
+
* notification is never a reply). A connect failure rejects `connect`,
|
|
10
18
|
* never affecting any other server (per-server isolation).
|
|
11
19
|
*
|
|
12
20
|
* Per-request timeout (codemode parity, spec slice 3): every request REJECTS
|
|
@@ -14,35 +22,31 @@
|
|
|
14
22
|
* awaiting a hung server forever. A `notifications/progress` frame naming a
|
|
15
23
|
* pending request's id (sent as the request's `progressToken`) RESETS that
|
|
16
24
|
* request's deadline — a server that keeps reporting progress stays alive
|
|
17
|
-
* as long as it works. A timeout rejects the call but never kills the
|
|
18
|
-
* (per-server isolation: the next request gets a fresh chance).
|
|
25
|
+
* as long as it works. A timeout rejects the call but never kills the
|
|
26
|
+
* transport (per-server isolation: the next request gets a fresh chance).
|
|
19
27
|
*/
|
|
20
|
-
import { type ChildProcess } from "node:child_process";
|
|
21
28
|
import type { McpServerConfig, McpToolDefinition, McpToolResult } from "./types.js";
|
|
22
|
-
/**
|
|
23
|
-
export
|
|
24
|
-
#private;
|
|
25
|
-
/** @internal constructed by {@link connectMcpServer}. */
|
|
26
|
-
constructor(name: string, child: ChildProcess);
|
|
27
|
-
/**
|
|
28
|
-
* The MCP handshake: `initialize` then `notifications/initialized`.
|
|
29
|
-
* The initialize reply is validated to carry the protocol version.
|
|
30
|
-
*/
|
|
31
|
-
handshake(): Promise<void>;
|
|
32
|
-
/** Discover the server's tools (`tools/list`). */
|
|
29
|
+
/** The public client surface (what the extension consumes). */
|
|
30
|
+
export interface McpClient {
|
|
33
31
|
listTools(): Promise<McpToolDefinition[]>;
|
|
34
|
-
/** Execute one tool (`tools/call`); an `isError` result resolves (it is a
|
|
35
|
-
* valid tool outcome, not a protocol failure). `timeoutSeconds` caps the
|
|
36
|
-
* wait per request (default 60; progress notifications reset it). */
|
|
37
32
|
callTool(name: string, args: Record<string, unknown>, timeoutSeconds?: number): Promise<McpToolResult>;
|
|
38
|
-
/**
|
|
39
|
-
exited(): Promise<Error | undefined>;
|
|
40
|
-
/** Kill the child and tear down (idempotent). */
|
|
33
|
+
/** Tear the connection down (idempotent). */
|
|
41
34
|
close(): void;
|
|
42
35
|
}
|
|
36
|
+
/** The byte streams of a core-launched per-user service. */
|
|
37
|
+
export interface McpServiceHandle {
|
|
38
|
+
readonly stdin: WritableStream<Uint8Array>;
|
|
39
|
+
readonly stdout: ReadableStream<Uint8Array>;
|
|
40
|
+
readonly exited: Promise<{
|
|
41
|
+
code: number | null;
|
|
42
|
+
signal: string | null;
|
|
43
|
+
}>;
|
|
44
|
+
release(): void;
|
|
45
|
+
}
|
|
43
46
|
/**
|
|
44
|
-
* Spawn
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
+
* Spawn/connect one configured MCP server (stdio or remote by config
|
|
48
|
+
* shape). Throws on spawn/connect failure or a failed handshake —
|
|
49
|
+
* per-server isolation is the CALLER's policy (the extension connects all
|
|
50
|
+
* servers with `.catch` per server).
|
|
47
51
|
*/
|
|
48
|
-
export declare function connectMcpServer(name: string, config: McpServerConfig): Promise<McpClient>;
|
|
52
|
+
export declare function connectMcpServer(name: string, config: McpServerConfig, handle?: McpServiceHandle): Promise<McpClient>;
|