@jini-ai/mcp 0.3.3 → 0.4.0

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.
Files changed (95) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +97 -22
  3. package/dist/agent-install/install.d.ts +20 -4
  4. package/dist/agent-install/install.js +11 -11
  5. package/dist/bin/serve.d.ts +25 -13
  6. package/dist/bin/serve.js +42 -21
  7. package/dist/client/client.d.ts +28 -8
  8. package/dist/client/client.js +25 -21
  9. package/dist/core/config.d.ts +57 -18
  10. package/dist/core/config.js +76 -48
  11. package/dist/core/index.d.ts +1 -2
  12. package/dist/core/index.js +1 -2
  13. package/dist/core/install-info.d.ts +4 -1
  14. package/dist/core/install-info.js +5 -5
  15. package/dist/core/tokens.d.ts +39 -13
  16. package/dist/core/tokens.js +54 -24
  17. package/dist/federation/adapter.http.d.ts +67 -0
  18. package/dist/federation/adapter.http.js +414 -0
  19. package/dist/federation/approvals/call-confirmation.d.ts +23 -0
  20. package/dist/federation/approvals/call-confirmation.js +124 -0
  21. package/dist/federation/approvals/index.d.ts +4 -0
  22. package/dist/federation/approvals/index.js +4 -0
  23. package/dist/federation/approvals/ports.d.ts +107 -0
  24. package/dist/federation/approvals/ports.js +2 -0
  25. package/dist/federation/approvals/revocation.d.ts +61 -0
  26. package/dist/federation/approvals/revocation.js +52 -0
  27. package/dist/federation/bootstrap.d.ts +100 -0
  28. package/dist/federation/bootstrap.js +182 -0
  29. package/dist/federation/config.d.ts +68 -0
  30. package/dist/federation/config.js +40 -0
  31. package/dist/federation/index.d.ts +14 -0
  32. package/dist/federation/index.js +14 -0
  33. package/dist/federation/mcp-protocol.d.ts +115 -0
  34. package/dist/federation/mcp-protocol.js +151 -0
  35. package/dist/federation/messages.d.ts +54 -0
  36. package/dist/federation/messages.js +35 -0
  37. package/dist/federation/ports.d.ts +287 -0
  38. package/dist/federation/ports.js +5 -0
  39. package/dist/federation/presets.d.ts +66 -0
  40. package/dist/federation/presets.js +30 -0
  41. package/dist/federation/refusal-notice.d.ts +198 -0
  42. package/dist/federation/refusal-notice.js +193 -0
  43. package/dist/federation/registrations.d.ts +171 -0
  44. package/dist/federation/registrations.js +181 -0
  45. package/dist/federation/reload.d.ts +112 -0
  46. package/dist/federation/reload.js +110 -0
  47. package/dist/federation/stdio/adapter.stdio.d.ts +157 -0
  48. package/dist/federation/stdio/adapter.stdio.js +526 -0
  49. package/dist/federation/stdio/default-connect.d.ts +29 -0
  50. package/dist/federation/stdio/default-connect.js +92 -0
  51. package/dist/federation/stdio/index.d.ts +4 -0
  52. package/dist/federation/stdio/index.js +4 -0
  53. package/dist/federation/stdio/stdio-launch-resolver.d.ts +139 -0
  54. package/dist/federation/stdio/stdio-launch-resolver.js +227 -0
  55. package/dist/federation/testing/adapter.memory.d.ts +177 -0
  56. package/dist/federation/testing/adapter.memory.js +189 -0
  57. package/dist/federation/testing/index.d.ts +3 -0
  58. package/dist/federation/testing/index.js +3 -0
  59. package/dist/federation/testing/tool-approvals.memory.d.ts +27 -0
  60. package/dist/federation/testing/tool-approvals.memory.js +38 -0
  61. package/dist/federation/tool-approvals.d.ts +97 -0
  62. package/dist/federation/tool-approvals.js +42 -0
  63. package/dist/federation/trust.d.ts +471 -0
  64. package/dist/federation/trust.js +707 -0
  65. package/dist/index.d.ts +175 -2
  66. package/dist/index.js +172 -2
  67. package/dist/server/daemon-client.d.ts +16 -4
  68. package/dist/server/daemon-client.js +41 -19
  69. package/dist/server/ports.d.ts +9 -0
  70. package/dist/server/ports.js +2 -0
  71. package/dist/server/resource-protocol.d.ts +14 -4
  72. package/dist/server/resource-protocol.js +6 -6
  73. package/dist/server/resources/active-resource.js +3 -3
  74. package/dist/server/sdk-transport.d.ts +11 -0
  75. package/dist/server/sdk-transport.js +31 -0
  76. package/dist/server/tool-protocol.d.ts +31 -9
  77. package/dist/server/tool-protocol.js +20 -18
  78. package/dist/server/tool-server.d.ts +60 -43
  79. package/dist/server/tool-server.js +51 -27
  80. package/dist/server/tools/component-catalog-tools.js +7 -7
  81. package/dist/server/tools/delegated-tool.d.ts +7 -4
  82. package/dist/server/tools/delegated-tool.js +14 -15
  83. package/dist/server/tools/run-tools.js +15 -15
  84. package/dist/server/tools/tool-catalog-tools.js +7 -7
  85. package/dist/tools/ask-choice/index.d.ts +123 -0
  86. package/dist/tools/ask-choice/index.js +98 -0
  87. package/dist/tools/ask-choice/pending.d.ts +11 -0
  88. package/dist/tools/ask-choice/pending.js +34 -0
  89. package/dist/tools/ask-choice/question.d.ts +16 -0
  90. package/dist/tools/ask-choice/question.js +60 -0
  91. package/dist/tools/ask-choice/schema.d.ts +91 -0
  92. package/dist/tools/ask-choice/schema.js +27 -0
  93. package/dist/tools/ask-choice/types.d.ts +106 -0
  94. package/dist/tools/ask-choice/types.js +2 -0
  95. package/package.json +43 -5
@@ -0,0 +1,139 @@
1
+ import type { FederationMessages } from "../messages.js";
2
+ import type { McpStdioLaunchSpec } from "../ports.js";
3
+ /**
4
+ * @file Desktop-only rewriting of stdio MCP launch specs so `npx` / `npm` / `node` (and other bare
5
+ * commands such as `uvx`/`docker`) resolve to something real inside a packaged Electron app, which
6
+ * ships no system `node` binary and whose Dock-launched PATH is `/usr/bin:/bin:/usr/sbin:/sbin`.
7
+ *
8
+ * Everything here is pure: no `spawn`, no logging, `fs` reached only through the injected
9
+ * `isExecutable`/`exists` seams (real `fs` by default). The impurity — writing the toolchain
10
+ * shims themselves, choosing the plan's env vars — is the host's toolchain resolver's job,
11
+ * not this file's. This module only answers "given this spec and this environment, what do we
12
+ * actually spawn, and with what extra env".
13
+ *
14
+ * Non-desktop deployments never set either env var {@link stdioLaunchResolverFromEnv} reads, so
15
+ * every caller keeps getting {@link IDENTITY_STDIO_LAUNCH_RESOLVER} — spawn args and child env
16
+ * byte-identical to before this module existed.
17
+ */
18
+ /**
19
+ * The three directory names form the host toolchain resolver contract. Sharing layout
20
+ * rather than importing a host implementation keeps the launcher independent of app shells.
21
+ * The host's toolchain resolver must create its configured toolchain directory with exactly
22
+ * these three children: a `bin/` holding the shims, an `npm-cache/`, and an `npm-prefix/`.
23
+ */
24
+ export declare const NODE_TOOLCHAIN_LAYOUT: {
25
+ readonly bin: "bin";
26
+ readonly npmCache: "npm-cache";
27
+ readonly npmPrefix: "npm-prefix";
28
+ };
29
+ /** Thrown instead of spawning when a stdio command cannot be resolved to anything executable —
30
+ * neither rewritten to the bundled Node toolchain, nor found on the child's search path, nor (for
31
+ * an already-absolute command) present on disk. The message is written for a person reading a
32
+ * connection-failed notice, not a stack trace, per §2 of the desktop-npx plan. */
33
+ export declare class McpLaunchUnavailableError extends Error {
34
+ constructor({ message }: {
35
+ message: string;
36
+ });
37
+ }
38
+ /**
39
+ * What {@link McpStdioLaunchResolver.resolve} hands back: the spec {@link McpStdioLaunchSpec}
40
+ * ready to `spawn`, plus `launchEnv` — the resolver's own additions, layered as
41
+ * `inherited < launchEnv < specEnv` by `buildMcpChildEnv`'s caller (a later slice; not built here).
42
+ *
43
+ * `env` is the connection's own {@link McpStdioLaunchSpec.env}, passed through unchanged — this
44
+ * resolver never edits it, only adds `launchEnv` alongside it.
45
+ *
46
+ * `warning` is set only by {@link stdioLaunchResolverFromEnv} falling back to identity with explicit
47
+ * host permission because the bundled toolchain is incomplete; it is returned rather than logged because this whole module stays
48
+ * pure, and logging is the caller's job.
49
+ */
50
+ export interface ResolvedStdioLaunch {
51
+ readonly command: string;
52
+ readonly args: readonly string[];
53
+ readonly cwd?: string | undefined;
54
+ readonly env: Readonly<Record<string, string>>;
55
+ readonly launchEnv: Readonly<Record<string, string>>;
56
+ /** Set only when `command` was rewritten (desktop `npx` -> Electron + `npx-cli.js`): the command as
57
+ * configured, so a decision about WHAT runs — `resolveStdioChildCwd`'s package-runner check — is
58
+ * not made on the Electron binary standing in for it. */
59
+ readonly requestedCommand?: string | undefined;
60
+ readonly warning?: string | undefined;
61
+ }
62
+ /** The desktop-only launch-rewriting port. Constructed by {@link createBundledNodeLaunchResolver}
63
+ * or handed the identity behaviour by {@link IDENTITY_STDIO_LAUNCH_RESOLVER}; selected between the
64
+ * two by {@link stdioLaunchResolverFromEnv}. */
65
+ export interface McpStdioLaunchResolver {
66
+ resolve({ spec }: {
67
+ spec: McpStdioLaunchSpec;
68
+ }): ResolvedStdioLaunch;
69
+ }
70
+ /** Every non-desktop deployment's resolver: the spec unchanged, no extra env. This is what makes
71
+ * the desktop rewrite opt-in by construction rather than by a runtime check scattered through the
72
+ * federation layer — nothing downstream can tell this resolver apart from "no resolver at all". */
73
+ export declare const IDENTITY_STDIO_LAUNCH_RESOLVER: McpStdioLaunchResolver;
74
+ /** {@link createBundledNodeLaunchResolver}'s config. Only `toolchainDir` and `npmRoot` are ever
75
+ * passed in production; the rest are test seams that default to the real process. */
76
+ export interface BundledNodeLaunchResolverConfig {
77
+ /** `toolchain env option`: holds `bin/` (shims), `npm-cache/`, `npm-prefix/`. */
78
+ readonly messages: FederationMessages;
79
+ readonly toolchainDir: string;
80
+ /** `npm root env option`: the npm package root; `<npmRoot>/bin/npx-cli.js` must exist (checked
81
+ * by {@link stdioLaunchResolverFromEnv} before this is constructed, not here). */
82
+ readonly npmRoot: string;
83
+ }
84
+ export interface BundledNodeLaunchResolverOptions {
85
+ readonly execPath?: string | undefined;
86
+ readonly platform?: NodeJS.Platform | undefined;
87
+ readonly parentEnv?: NodeJS.ProcessEnv | undefined;
88
+ readonly isExecutable?: ((required: {
89
+ candidatePath: string;
90
+ }) => boolean) | undefined;
91
+ }
92
+ /**
93
+ * The desktop resolver: rewrites `npx`/`npm`/`node` to run on Electron's own Node via the bundled
94
+ * npm package, resolves any other bare command (`uvx`, `docker`, …) against a PATH widened with
95
+ * the toolchain's shim `bin/` and (darwin) a few well-known install dirs, leaves a path command
96
+ * (absolute, or relative to `spec.cwd`)'s `command`/`args` unchanged, and throws {@link McpLaunchUnavailableError}
97
+ * before any spawn when nothing resolves — see plan §2's table for the full case list.
98
+ *
99
+ * `ELECTRON_RUN_AS_NODE` is added to `launchEnv` only for the three rewritten commands, i.e. only
100
+ * when `command` becomes `execPath` — never for a third-party binary found on PATH or given as an
101
+ * absolute path, which must run as themselves, not as Electron-running-as-Node.
102
+ *
103
+ * @complexity O(1) to construct; `resolve` is O(n) in the search path's length.
104
+ */
105
+ export declare function createBundledNodeLaunchResolver({ messages, toolchainDir, npmRoot, }: BundledNodeLaunchResolverConfig, { execPath, platform, parentEnv, isExecutable, }?: BundledNodeLaunchResolverOptions): McpStdioLaunchResolver;
106
+ /** {@link stdioLaunchResolverFromEnv}'s options; `exists` is a test seam, defaulting to a real
107
+ * filesystem check. */
108
+ export interface StdioLaunchResolverFromEnvOptions {
109
+ readonly exists?: ((required: {
110
+ candidatePath: string;
111
+ }) => boolean) | undefined;
112
+ /** Explicit host opt-in to using PATH when a configured bundled toolchain is incomplete.
113
+ * Without it, a broken desktop bundle refuses launch rather than running a different binary.
114
+ */
115
+ readonly allowIdentityFallback?: boolean | undefined;
116
+ }
117
+ /**
118
+ * Picks the resolver from an environment: with both `toolchain env option` and
119
+ * `npm root env option` unset — every non-desktop deployment —
120
+ * {@link IDENTITY_STDIO_LAUNCH_RESOLVER}. The original plan §2 silently fell back when either
121
+ * variable was missing or `<npmRoot>/bin/npx-cli.js` was absent (a desktop build gone wrong).
122
+ * That could execute a different PATH binary than the configured bundle. Incomplete configured
123
+ * toolchains now throw unless the host explicitly permits identity fallback. Permitted fallbacks
124
+ * carry a `warning` through every resolution so a caller can log it once rather than this pure
125
+ * function logging on its own. Otherwise, {@link createBundledNodeLaunchResolver}.
126
+ *
127
+ * `env` doubles as the resolver's `parentEnv` (its PATH/HOME feed {@link buildSearchDirs}) — the
128
+ * same object `attachFederatedMcpTools` already has as `params.env ?? process.env`, so this
129
+ * function needs no second environment threaded in separately.
130
+ *
131
+ * @complexity O(1) beyond the `createBundledNodeLaunchResolver` call it may make.
132
+ */
133
+ export declare function stdioLaunchResolverFromEnv({ env, toolchainDirEnvVar, npmRootEnvVar, messages }: {
134
+ env: NodeJS.ProcessEnv;
135
+ toolchainDirEnvVar: string;
136
+ npmRootEnvVar: string;
137
+ messages: FederationMessages;
138
+ }, { exists, allowIdentityFallback }?: StdioLaunchResolverFromEnvOptions): McpStdioLaunchResolver;
139
+ //# sourceMappingURL=stdio-launch-resolver.d.ts.map
@@ -0,0 +1,227 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * @file Desktop-only rewriting of stdio MCP launch specs so `npx` / `npm` / `node` (and other bare
5
+ * commands such as `uvx`/`docker`) resolve to something real inside a packaged Electron app, which
6
+ * ships no system `node` binary and whose Dock-launched PATH is `/usr/bin:/bin:/usr/sbin:/sbin`.
7
+ *
8
+ * Everything here is pure: no `spawn`, no logging, `fs` reached only through the injected
9
+ * `isExecutable`/`exists` seams (real `fs` by default). The impurity — writing the toolchain
10
+ * shims themselves, choosing the plan's env vars — is the host's toolchain resolver's job,
11
+ * not this file's. This module only answers "given this spec and this environment, what do we
12
+ * actually spawn, and with what extra env".
13
+ *
14
+ * Non-desktop deployments never set either env var {@link stdioLaunchResolverFromEnv} reads, so
15
+ * every caller keeps getting {@link IDENTITY_STDIO_LAUNCH_RESOLVER} — spawn args and child env
16
+ * byte-identical to before this module existed.
17
+ */
18
+ /**
19
+ * The three directory names form the host toolchain resolver contract. Sharing layout
20
+ * rather than importing a host implementation keeps the launcher independent of app shells.
21
+ * The host's toolchain resolver must create its configured toolchain directory with exactly
22
+ * these three children: a `bin/` holding the shims, an `npm-cache/`, and an `npm-prefix/`.
23
+ */
24
+ export const NODE_TOOLCHAIN_LAYOUT = {
25
+ bin: "bin",
26
+ npmCache: "npm-cache",
27
+ npmPrefix: "npm-prefix",
28
+ };
29
+ /** Thrown instead of spawning when a stdio command cannot be resolved to anything executable —
30
+ * neither rewritten to the bundled Node toolchain, nor found on the child's search path, nor (for
31
+ * an already-absolute command) present on disk. The message is written for a person reading a
32
+ * connection-failed notice, not a stack trace, per §2 of the desktop-npx plan. */
33
+ export class McpLaunchUnavailableError extends Error {
34
+ constructor({ message }) {
35
+ super(message);
36
+ this.name = "McpLaunchUnavailableError";
37
+ }
38
+ }
39
+ /** Every non-desktop deployment's resolver: the spec unchanged, no extra env. This is what makes
40
+ * the desktop rewrite opt-in by construction rather than by a runtime check scattered through the
41
+ * federation layer — nothing downstream can tell this resolver apart from "no resolver at all". */
42
+ export const IDENTITY_STDIO_LAUNCH_RESOLVER = {
43
+ resolve({ spec }) {
44
+ return { command: spec.command, args: spec.args, cwd: spec.cwd, env: spec.env, launchEnv: {} };
45
+ },
46
+ };
47
+ /** @complexity O(1). */
48
+ function matchNodeToolchainCommand(command, platform) {
49
+ const isWin32 = platform === "win32";
50
+ if (command === "npx" || (isWin32 && command === "npx.cmd"))
51
+ return "npx";
52
+ if (command === "npm" || (isWin32 && command === "npm.cmd"))
53
+ return "npm";
54
+ if (command === "node" || (isWin32 && command === "node.exe"))
55
+ return "node";
56
+ return null;
57
+ }
58
+ /**
59
+ * The child's search path, in the plan §2 order: the toolchain's own `bin/` first (so the shims
60
+ * always win), then the parent's PATH, then — darwin only — the well-known dirs a Homebrew or
61
+ * cargo/uv install lands in, which a Dock launch's minimal PATH never includes. Well-known dirs are
62
+ * APPENDED, so anything already on the parent PATH still wins, and `$HOME`-rooted ones are skipped
63
+ * outright when `parentEnv.HOME` is unset rather than joined against `"undefined"`.
64
+ *
65
+ * @complexity O(n) in the parent PATH's entry count.
66
+ */
67
+ function buildSearchDirs(binDir, parentEnv, platform) {
68
+ const delimiter = platform === "win32" ? ";" : ":";
69
+ const parentDirs = (parentEnv.PATH ?? "").split(delimiter).filter((entry) => entry.length > 0);
70
+ const wellKnown = [];
71
+ if (platform === "darwin") {
72
+ wellKnown.push("/opt/homebrew/bin", "/usr/local/bin");
73
+ const home = parentEnv.HOME;
74
+ if (home)
75
+ wellKnown.push(`${home}/.local/bin`, `${home}/.cargo/bin`);
76
+ }
77
+ const seen = new Set();
78
+ const deduped = [];
79
+ for (const dir of [binDir, ...parentDirs, ...wellKnown]) {
80
+ if (seen.has(dir))
81
+ continue;
82
+ seen.add(dir);
83
+ deduped.push(dir);
84
+ }
85
+ return deduped;
86
+ }
87
+ /** The 8 `npm_config_*` variables from plan §3: a cache and prefix confined to the toolchain
88
+ * directory (never `~/.npm`, never a global `/usr/local`), and quiet, fast-failing network
89
+ * behaviour so a cold desktop launch neither prints noise nor outlasts the connect timeout.
90
+ * @complexity O(1). */
91
+ function buildNpmConfigEnv(toolchainDir, pathModule) {
92
+ return {
93
+ npm_config_cache: pathModule.join(toolchainDir, NODE_TOOLCHAIN_LAYOUT.npmCache),
94
+ npm_config_prefix: pathModule.join(toolchainDir, NODE_TOOLCHAIN_LAYOUT.npmPrefix),
95
+ npm_config_update_notifier: "false",
96
+ npm_config_fund: "false",
97
+ npm_config_audit: "false",
98
+ npm_config_fetch_retries: "1",
99
+ npm_config_fetch_retry_mintimeout: "2000",
100
+ npm_config_fetch_retry_maxtimeout: "5000",
101
+ };
102
+ }
103
+ /** Default `isExecutable`: a real filesystem check, X_OK bit, exceptions swallowed to `false` —
104
+ * "can't tell" and "not executable" get the same answer, since both mean "don't spawn this".
105
+ * @complexity O(1). */
106
+ function defaultIsExecutable(candidatePath) {
107
+ try {
108
+ fs.accessSync(candidatePath, fs.constants.X_OK);
109
+ return true;
110
+ }
111
+ catch {
112
+ return false;
113
+ }
114
+ }
115
+ /** Searches `searchDirs` in order for an executable file named `command`. On win32 an
116
+ * extensionless name is also tried as `<name>.exe`, the one suffix `spawn` itself adds there
117
+ * without a shell — so `uvx`/`docker` resolve to `uvx.exe`/`docker.exe` as they did before this
118
+ * resolver existed. @complexity O(n) in `searchDirs.length`. */
119
+ function findOnSearchDirs(command, searchDirs, pathModule, isExecutable) {
120
+ const names = pathModule === path.win32 && pathModule.extname(command) === "" ? [command, `${command}.exe`] : [command];
121
+ for (const dir of searchDirs) {
122
+ for (const name of names) {
123
+ const candidate = pathModule.join(dir, name);
124
+ if (isExecutable({ candidatePath: candidate }))
125
+ return candidate;
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+ /** Whether `command` names a path (absolute, or relative with a separator such as `./bin/server`)
131
+ * rather than a bare name to search for — the same split `spawn`/`execvp` makes. @complexity O(n)
132
+ * in the command's length. */
133
+ function isPathCommand(command, pathModule) {
134
+ return pathModule.isAbsolute(command) || command.includes("/") || (pathModule === path.win32 && command.includes("\\"));
135
+ }
136
+ /**
137
+ * The desktop resolver: rewrites `npx`/`npm`/`node` to run on Electron's own Node via the bundled
138
+ * npm package, resolves any other bare command (`uvx`, `docker`, …) against a PATH widened with
139
+ * the toolchain's shim `bin/` and (darwin) a few well-known install dirs, leaves a path command
140
+ * (absolute, or relative to `spec.cwd`)'s `command`/`args` unchanged, and throws {@link McpLaunchUnavailableError}
141
+ * before any spawn when nothing resolves — see plan §2's table for the full case list.
142
+ *
143
+ * `ELECTRON_RUN_AS_NODE` is added to `launchEnv` only for the three rewritten commands, i.e. only
144
+ * when `command` becomes `execPath` — never for a third-party binary found on PATH or given as an
145
+ * absolute path, which must run as themselves, not as Electron-running-as-Node.
146
+ *
147
+ * @complexity O(1) to construct; `resolve` is O(n) in the search path's length.
148
+ */
149
+ export function createBundledNodeLaunchResolver({ messages, toolchainDir, npmRoot, }, { execPath = process.execPath, platform = process.platform, parentEnv = process.env, isExecutable = ({ candidatePath }) => defaultIsExecutable(candidatePath), } = {}) {
150
+ const pathModule = platform === "win32" ? path.win32 : path.posix;
151
+ const binDir = pathModule.join(toolchainDir, NODE_TOOLCHAIN_LAYOUT.bin);
152
+ const searchDirs = buildSearchDirs(binDir, parentEnv, platform);
153
+ const pathValue = searchDirs.join(platform === "win32" ? ";" : ":");
154
+ const npmConfigEnv = buildNpmConfigEnv(toolchainDir, pathModule);
155
+ const nodeToolchainLaunchEnv = { ELECTRON_RUN_AS_NODE: "1", PATH: pathValue, ...npmConfigEnv };
156
+ const otherLaunchEnv = { PATH: pathValue, ...npmConfigEnv };
157
+ return {
158
+ resolve({ spec }) {
159
+ const nodeToolchainCommand = matchNodeToolchainCommand(spec.command, platform);
160
+ if (nodeToolchainCommand !== null) {
161
+ const cli = nodeToolchainCommand === "npx" ? "npx-cli.js" : nodeToolchainCommand === "npm" ? "npm-cli.js" : null;
162
+ const args = cli === null ? [...spec.args] : [pathModule.join(npmRoot, "bin", cli), ...spec.args];
163
+ return { command: execPath, args, cwd: spec.cwd, env: spec.env, launchEnv: nodeToolchainLaunchEnv, requestedCommand: spec.command };
164
+ }
165
+ if (isPathCommand(spec.command, pathModule)) {
166
+ // A relative path runs against the child's own cwd, so that is where it has to exist.
167
+ if (!isExecutable({ candidatePath: pathModule.resolve(spec.cwd ?? process.cwd(), spec.command) })) {
168
+ throw new McpLaunchUnavailableError({ message: messages.launchUnavailable({ command: spec.command, searchedDirs: searchDirs }) });
169
+ }
170
+ return { command: spec.command, args: [...spec.args], cwd: spec.cwd, env: spec.env, launchEnv: otherLaunchEnv };
171
+ }
172
+ const found = findOnSearchDirs(spec.command, searchDirs, pathModule, isExecutable);
173
+ if (found === null) {
174
+ throw new McpLaunchUnavailableError({ message: messages.launchUnavailable({ command: spec.command, searchedDirs: searchDirs }) });
175
+ }
176
+ return { command: found, args: [...spec.args], cwd: spec.cwd, env: spec.env, launchEnv: otherLaunchEnv };
177
+ },
178
+ };
179
+ }
180
+ /**
181
+ * Picks the resolver from an environment: with both `toolchain env option` and
182
+ * `npm root env option` unset — every non-desktop deployment —
183
+ * {@link IDENTITY_STDIO_LAUNCH_RESOLVER}. The original plan §2 silently fell back when either
184
+ * variable was missing or `<npmRoot>/bin/npx-cli.js` was absent (a desktop build gone wrong).
185
+ * That could execute a different PATH binary than the configured bundle. Incomplete configured
186
+ * toolchains now throw unless the host explicitly permits identity fallback. Permitted fallbacks
187
+ * carry a `warning` through every resolution so a caller can log it once rather than this pure
188
+ * function logging on its own. Otherwise, {@link createBundledNodeLaunchResolver}.
189
+ *
190
+ * `env` doubles as the resolver's `parentEnv` (its PATH/HOME feed {@link buildSearchDirs}) — the
191
+ * same object `attachFederatedMcpTools` already has as `params.env ?? process.env`, so this
192
+ * function needs no second environment threaded in separately.
193
+ *
194
+ * @complexity O(1) beyond the `createBundledNodeLaunchResolver` call it may make.
195
+ */
196
+ export function stdioLaunchResolverFromEnv({ env, toolchainDirEnvVar, npmRootEnvVar, messages }, { exists = ({ candidatePath }) => fs.existsSync(candidatePath), allowIdentityFallback = false } = {}) {
197
+ const toolchainDir = env[toolchainDirEnvVar];
198
+ const npmRoot = env[npmRootEnvVar];
199
+ if (toolchainDir === undefined && npmRoot === undefined)
200
+ return IDENTITY_STDIO_LAUNCH_RESOLVER;
201
+ if (!toolchainDir || !npmRoot) {
202
+ return configuredToolchainFallback({
203
+ message: `mcp-federation: ${toolchainDirEnvVar} and ${npmRootEnvVar} must both be set to non-empty paths.`,
204
+ allowIdentityFallback,
205
+ });
206
+ }
207
+ const npxCliPath = path.join(npmRoot, "bin", "npx-cli.js");
208
+ if (!exists({ candidatePath: npxCliPath })) {
209
+ return configuredToolchainFallback({
210
+ message: `mcp-federation: ${npmRootEnvVar} is set (${npmRoot}) but ${npxCliPath} is missing`,
211
+ allowIdentityFallback,
212
+ });
213
+ }
214
+ return createBundledNodeLaunchResolver({ toolchainDir, npmRoot, messages }, { parentEnv: env });
215
+ }
216
+ /** Keep fallback policy separate from resolution; only an explicit true can broaden execution. */
217
+ function configuredToolchainFallback({ message, allowIdentityFallback }) {
218
+ if (allowIdentityFallback !== true)
219
+ throw new McpLaunchUnavailableError({ message });
220
+ const warning = message + "; falling back to identity stdio launch resolution instead of the bundled Node toolchain.";
221
+ return {
222
+ resolve({ spec }) {
223
+ return { ...IDENTITY_STDIO_LAUNCH_RESOLVER.resolve({ spec }), warning };
224
+ },
225
+ };
226
+ }
227
+ //# sourceMappingURL=stdio-launch-resolver.js.map
@@ -0,0 +1,177 @@
1
+ import type { FederationMessages } from "../messages.js";
2
+ import type { McpToolCallRequiredArgs, McpToolCallOptions, McpHttpRequestRequiredArgs, McpHttpRequestOptions } from "../ports.js";
3
+ import type { McpHttpExchange, McpHttpResponse, McpSessionPort, McpStdioChannel, RemoteToolDescriptor, RemoteToolResult } from "../ports.js";
4
+ /**
5
+ * @file The in-memory doubles for both federation seams — rule-of-two's partner to
6
+ * `adapter.stdio.ts`, and this codebase's usual `repo.memory.ts`/`repo.sqlite.ts` split applied to
7
+ * an outbound protocol boundary instead of a database.
8
+ *
9
+ * One double per seam (see `ports.ts`), and they test different things:
10
+ *
11
+ * - {@link InMemoryMcpSession} fakes `McpSessionPort`, i.e. "an MCP server exists and behaves". It
12
+ * is what `registrations.ts`'s tests use, so the trust tier and the registration wiring are
13
+ * exercised with no protocol in the picture at all.
14
+ *
15
+ * - {@link ScriptedMcpStdioChannel} fakes `McpStdioChannel`, i.e. raw newline-delimited JSON-RPC.
16
+ * It is what `adapter.stdio.ts`'s OWN tests use, so the real client's handshake ordering,
17
+ * pagination, id correlation, timeout and close behaviour are exercised against something that
18
+ * can also misbehave on purpose — reply out of order, reply late, reply twice, never reply,
19
+ * send an unsolicited request, or die mid-flight.
20
+ *
21
+ * - {@link ScriptedMcpHttpExchange} does the same for `McpHttpExchange`, the hosted transport's
22
+ * seam, so `adapter.http.ts` is under test on the same terms.
23
+ *
24
+ * Those inner-seam doubles are the ones that make the "no live third-party server in this sandbox"
25
+ * constraint survivable: the protocol code under test is the real production code, and only the
26
+ * pipe beneath it is fake. A double at the outer seam alone would have proved only that the double
27
+ * works.
28
+ *
29
+ * Architectural role:
30
+ * Test doubles. Ships in `src/` beside the port they implement, exactly as every feature's
31
+ * `repo.memory.ts` does, so they are importable from `__tests__/` without a second tsconfig.
32
+ * See docs/decisions/DR-003-trusted-federation-composition.md.
33
+ */
34
+ /**
35
+ * A scripted `McpSessionPort`: a fixed tool list plus a per-tool call handler.
36
+ *
37
+ * `callTool` records every call, so a test can assert not just the returned value but that the
38
+ * REMOTE NAME was sent — the property that proves namespacing is applied on the way in and stripped
39
+ * on the way out, which is the single most load-bearing behaviour in the registration layer.
40
+ */
41
+ export declare class InMemoryMcpSession implements McpSessionPort {
42
+ readonly calls: Array<{
43
+ name: string;
44
+ arguments: Record<string, unknown>;
45
+ }>;
46
+ closed: boolean;
47
+ private readonly tools;
48
+ private readonly handler;
49
+ private readonly listToolsBehaviour;
50
+ constructor({ tools }: {
51
+ tools: readonly RemoteToolDescriptor[];
52
+ }, options?: {
53
+ /** Defaults to echoing the call back, which is enough for most wiring assertions. */
54
+ onCall?: ((required: {
55
+ name: string;
56
+ args: Record<string, unknown>;
57
+ }) => RemoteToolResult | Promise<RemoteToolResult>) | undefined;
58
+ /** Overrides `listTools` wholesale, for the "the remote is down at connect time" cases. */
59
+ onListTools?: (() => Promise<RemoteToolDescriptor[]>) | undefined;
60
+ });
61
+ listTools(): Promise<RemoteToolDescriptor[]>;
62
+ callTool(request: McpToolCallRequiredArgs, _options?: McpToolCallOptions): Promise<RemoteToolResult>;
63
+ close(_required: Record<string, never>): Promise<void>;
64
+ }
65
+ /** One JSON-RPC message the fake channel received from the client under test. */
66
+ export interface CapturedRpcMessage {
67
+ jsonrpc?: string | undefined;
68
+ id?: number | undefined;
69
+ method?: string | undefined;
70
+ params?: Record<string, unknown> | undefined;
71
+ error?: {
72
+ code?: number | undefined;
73
+ message?: string | undefined;
74
+ } | undefined;
75
+ }
76
+ /**
77
+ * A fake {@link McpStdioChannel} that plays the SERVER side of MCP, under the test's control.
78
+ *
79
+ * `respond` is a plain function of the received message, so a test scripts a server by writing one:
80
+ * return a response object to reply, or `undefined` to stay silent (the case that exercises the
81
+ * client's timeout path). Everything the client sends is captured in {@link sent}, which is how
82
+ * handshake ordering is asserted — that `initialize` comes first and that
83
+ * `notifications/initialized` follows it with no `id`.
84
+ */
85
+ export declare class ScriptedMcpStdioChannel implements McpStdioChannel {
86
+ /** Every message the client wrote, parsed, in order. */
87
+ readonly sent: CapturedRpcMessage[];
88
+ closedReason: string | null;
89
+ private readonly messageListeners;
90
+ private readonly closeListeners;
91
+ private readonly respond;
92
+ private readonly messages;
93
+ constructor(options: {
94
+ /** Return a JSON-RPC response object to reply with, or `undefined` for silence. */
95
+ respond: (required: {
96
+ message: CapturedRpcMessage;
97
+ }) => unknown;
98
+ messages: FederationMessages;
99
+ });
100
+ send({ message }: {
101
+ message: string;
102
+ }): void;
103
+ onMessage({ listener }: {
104
+ listener: (required: {
105
+ message: string;
106
+ }) => void;
107
+ }): void;
108
+ onClose({ listener }: {
109
+ listener: (required: {
110
+ reason: string;
111
+ }) => void;
112
+ }): void;
113
+ close(_required: Record<string, never>): void;
114
+ /** Pushes a message to the client out of band — for late replies, unsolicited server requests,
115
+ * and notifications. */
116
+ deliver({ message }: {
117
+ message: unknown;
118
+ }): void;
119
+ /** Simulates the far end dying. */
120
+ fail({ reason }: {
121
+ reason: string;
122
+ }): void;
123
+ /** The last id the client used for `method`, for building a matching reply. */
124
+ idFor({ method }: {
125
+ method: string;
126
+ }): number | undefined;
127
+ }
128
+ /** One request the fake exchange received from the client under test. */
129
+ export interface CapturedHttpRequest {
130
+ readonly url: string;
131
+ readonly method: "POST" | "DELETE";
132
+ readonly headers: Readonly<Record<string, string>>;
133
+ /** The parsed JSON-RPC frame, or `undefined` for a bodyless request. */
134
+ readonly message: CapturedRpcMessage | undefined;
135
+ }
136
+ /** What a scripted server answers with. Defaults fill in the boring parts so a test states only
137
+ * the field it is actually exercising. */
138
+ export interface ScriptedHttpReply {
139
+ readonly status?: number | undefined;
140
+ readonly contentType?: string | undefined;
141
+ readonly sessionId?: string | undefined;
142
+ readonly wwwAuthenticate?: string | undefined;
143
+ /** A JSON-RPC object (serialized for the client), or a raw string for malformed-body cases. */
144
+ readonly body?: unknown | undefined;
145
+ }
146
+ /**
147
+ * A fake {@link McpHttpExchange} that plays the SERVER side of MCP's Streamable HTTP transport.
148
+ *
149
+ * The hosted counterpart to {@link ScriptedMcpStdioChannel}, and it exists for the same reason: so
150
+ * `adapter.http.ts`'s REAL handshake ordering, session-id propagation, SSE framing, pagination,
151
+ * status mapping and timeout behaviour are exercised against something that can misbehave on
152
+ * purpose — answer 401, answer with an unparseable body, issue a header-injecting session id,
153
+ * paginate forever, or never answer at all — rather than against a stub of the client itself.
154
+ *
155
+ * `respond` is a plain function of the received request, so a test scripts a server by writing one.
156
+ * Returning `undefined` means "never answer", which is how the timeout path is reached: the
157
+ * returned promise stays pending until the client's own `AbortSignal` fires.
158
+ */
159
+ export declare class ScriptedMcpHttpExchange implements McpHttpExchange {
160
+ /** Every request the client made, in order. */
161
+ readonly sent: CapturedHttpRequest[];
162
+ private readonly respond;
163
+ constructor(options: {
164
+ respond: (required: {
165
+ request: CapturedHttpRequest;
166
+ }) => ScriptedHttpReply | undefined;
167
+ });
168
+ send(request: McpHttpRequestRequiredArgs, options?: McpHttpRequestOptions): Promise<McpHttpResponse>;
169
+ /** A server that accepts the request and then goes quiet. Settles only when the client aborts,
170
+ * which is exactly what a real stalled connection does. */
171
+ private neverAnswer;
172
+ /** The last request whose JSON-RPC `method` matches, for asserting on what was sent. */
173
+ lastRequestFor({ method }: {
174
+ method: string;
175
+ }): CapturedHttpRequest | undefined;
176
+ }
177
+ //# sourceMappingURL=adapter.memory.d.ts.map