@jini-ai/mcp 0.4.2 → 0.4.4

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 (61) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -0
  3. package/dist/agent-install/chrome-devtools.d.ts +4 -0
  4. package/dist/agent-install/chrome-devtools.js +7 -0
  5. package/dist/agent-install/index.d.ts +1 -0
  6. package/dist/agent-install/index.js +1 -0
  7. package/dist/agent-install/install.js +102 -194
  8. package/dist/bin/serve.d.ts +4 -1
  9. package/dist/bin/serve.js +120 -45
  10. package/dist/client/client.js +71 -58
  11. package/dist/core/config.js +113 -149
  12. package/dist/core/tokens.js +29 -53
  13. package/dist/federation/adapter.http.js +3 -2
  14. package/dist/federation/approvals/call-confirmation.js +5 -70
  15. package/dist/federation/approvals/remembered-approval.d.ts +39 -0
  16. package/dist/federation/approvals/remembered-approval.js +98 -0
  17. package/dist/federation/approvals/revocation-reason.d.ts +9 -0
  18. package/dist/federation/approvals/revocation-reason.js +11 -0
  19. package/dist/federation/approvals/revocation.js +19 -13
  20. package/dist/federation/approvals/scope-option.d.ts +6 -0
  21. package/dist/federation/approvals/scope-option.js +6 -0
  22. package/dist/federation/bootstrap.d.ts +18 -0
  23. package/dist/federation/bootstrap.js +4 -4
  24. package/dist/federation/presets.d.ts +16 -17
  25. package/dist/federation/presets.js +35 -28
  26. package/dist/federation/stdio/adapter.stdio.js +14 -6
  27. package/dist/federation/stdio/default-connect.js +24 -27
  28. package/dist/federation/stdio/stdio-launch-resolver.d.ts +1 -1
  29. package/dist/federation/stdio/stdio-launch-resolver.js +13 -9
  30. package/dist/federation/tool-approvals.js +3 -1
  31. package/dist/federation/trust.d.ts +23 -2
  32. package/dist/federation/trust.js +69 -35
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +2 -2
  35. package/dist/server/daemon-client.d.ts +2 -0
  36. package/dist/server/daemon-client.js +86 -69
  37. package/dist/server/index.d.ts +1 -0
  38. package/dist/server/index.js +1 -0
  39. package/dist/server/resources/site-guide-resource.d.ts +15 -0
  40. package/dist/server/resources/site-guide-resource.js +25 -0
  41. package/dist/server/tool-arguments.d.ts +46 -0
  42. package/dist/server/tool-arguments.js +111 -0
  43. package/dist/server/tool-protocol.d.ts +62 -7
  44. package/dist/server/tool-protocol.js +30 -40
  45. package/dist/server/tool-server.d.ts +19 -0
  46. package/dist/server/tool-server.js +91 -19
  47. package/dist/server/tools/delegated-tool.d.ts +47 -0
  48. package/dist/server/tools/delegated-tool.js +35 -2
  49. package/dist/server/tools/tool-catalog-tools.js +30 -8
  50. package/dist/tools/ask-choice/index.d.ts +213 -14
  51. package/dist/tools/ask-choice/index.js +90 -46
  52. package/dist/tools/ask-choice/question.d.ts +5 -1
  53. package/dist/tools/ask-choice/question.js +36 -2
  54. package/dist/tools/ask-choice/schema.d.ts +196 -10
  55. package/dist/tools/ask-choice/schema.js +33 -12
  56. package/dist/tools/ask-choice/types.d.ts +8 -0
  57. package/package.json +6 -5
  58. package/dist/core/oauth.d.ts +0 -274
  59. package/dist/core/oauth.js +0 -667
  60. package/dist/core/secure-write.d.ts +0 -17
  61. package/dist/core/secure-write.js +0 -72
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @jini-ai/mcp
2
2
 
3
+ ## 0.4.4 — 2026-10-22
4
+
5
+ - 2026-10-22: Typed tools from the launched bridge, concrete describe schemas, list-changed, diagnostic error codes, coverage.
6
+
3
7
  ## 0.4.2
4
8
 
5
9
  ### Patch Changes
package/README.md CHANGED
@@ -58,6 +58,7 @@ engine nor retries a challenged request automatically.
58
58
  returning a discriminated `InstallPlan` (`CliInstallPlan` | `JsonInstallPlan` | `ManualInstallPlan`),
59
59
  and `applyJsonInstall` / `removeJsonInstall` to execute the JSON-file variant. Plus
60
60
  `buildMcpInstallPayload` for the install-info payload a UI renders.
61
+ For an explicit UI check, pass `CHROME_DEVTOOLS_MCP_PRESET` to `planAgentInstall` with `ctx.serverName: "chrome-devtools"`; it pins 1.10.1 and disables usage statistics and CrUX. See the [on-demand recipe](../diagnostics/docs/ui-performance-check.md).
61
62
 
62
63
  **Client-side runtime helpers** — `extractRelativeRefs`, `isTextualMime`.
63
64
 
@@ -0,0 +1,4 @@
1
+ import type { McpLaunchSpec } from './install.js';
2
+ /** Explicit on-demand launch preset; importing it never registers or launches a browser. */
3
+ export declare const CHROME_DEVTOOLS_MCP_PRESET: McpLaunchSpec;
4
+ //# sourceMappingURL=chrome-devtools.d.ts.map
@@ -0,0 +1,7 @@
1
+ /** Explicit on-demand launch preset; importing it never registers or launches a browser. */
2
+ export const CHROME_DEVTOOLS_MCP_PRESET = {
3
+ command: 'npx',
4
+ args: ['-y', 'chrome-devtools-mcp@1.10.1', '--no-usage-statistics', '--no-performance-crux'],
5
+ env: {},
6
+ };
7
+ //# sourceMappingURL=chrome-devtools.js.map
@@ -6,4 +6,5 @@
6
6
  * apply/remove primitives. Pure planning logic; no sibling MCP imports.
7
7
  */
8
8
  export * from './install.js';
9
+ export { CHROME_DEVTOOLS_MCP_PRESET } from './chrome-devtools.js';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -6,4 +6,5 @@
6
6
  * apply/remove primitives. Pure planning logic; no sibling MCP imports.
7
7
  */
8
8
  export * from './install.js';
9
+ export { CHROME_DEVTOOLS_MCP_PRESET } from './chrome-devtools.js';
9
10
  //# sourceMappingURL=index.js.map
@@ -84,205 +84,122 @@ function envFlags(env, flag) {
84
84
  }
85
85
  return out;
86
86
  }
87
+ // Attaches the env map under `key` only when non-empty, so configs stay clean.
88
+ function withEnv(entry, env, key) {
89
+ if (Object.keys(env).length > 0)
90
+ entry[key] = env;
91
+ return entry;
92
+ }
87
93
  /** JSON server entry shared by the `env`-style agents (cursor / copilot /
88
94
  * cline / openclaw / antigravity / trae). `extra` carries per-agent
89
95
  * fields; env is only attached when non-empty so configs stay clean. */
90
- function jsonEntry(spec, extra = {}, envKey = 'env') {
91
- const entry = {
92
- command: spec.command,
93
- args: spec.args,
94
- ...extra,
95
- };
96
- if (Object.keys(spec.env).length > 0) {
97
- entry[envKey] = spec.env;
98
- }
99
- return entry;
96
+ function jsonEntry(spec, extra = {}) {
97
+ return withEnv({ command: spec.command, args: spec.args, ...extra }, spec.env, 'env');
98
+ }
99
+ function cliPlan(slug, bin, argv, serverName) {
100
+ return { kind: 'cli', slug, bin, ...argv, getArgv: ['mcp', 'get', serverName] };
101
+ }
102
+ function jsonPlan(slug, configPath, keyPath, serverKey, entry) {
103
+ return { kind: 'json', slug, configPath, keyPath, serverKey, entry };
100
104
  }
105
+ // One planner per slug. Typed as a total map over `AgentSlug`, so adding a slug
106
+ // without a planner is a compile error (the role the old switch's `never` default played).
107
+ const AGENT_PLANNERS = {
108
+ // ----- CLI-driven agents (idempotent via the agent's own tool) -----
109
+ claude: ({ spec, ctx: { serverName } }) => cliPlan('claude', 'claude', {
110
+ addArgv: [
111
+ 'mcp', 'add', '--scope', 'user',
112
+ serverName,
113
+ ...envFlags(spec.env, '-e'),
114
+ '--', spec.command, ...spec.args,
115
+ ],
116
+ removeArgv: ['mcp', 'remove', '--scope', 'user', serverName],
117
+ }, serverName),
118
+ codex: ({ spec, ctx: { serverName } }) => cliPlan('codex', 'codex', {
119
+ addArgv: [
120
+ 'mcp', 'add', serverName,
121
+ ...envFlags(spec.env, '--env'),
122
+ '--', spec.command, ...spec.args,
123
+ ],
124
+ removeArgv: ['mcp', 'remove', serverName],
125
+ }, serverName),
126
+ kimi: ({ spec, ctx: { serverName } }) => cliPlan('kimi', 'kimi', {
127
+ addArgv: [
128
+ 'mcp', 'add', '--transport', 'stdio',
129
+ ...envFlags(spec.env, '--env'),
130
+ serverName, '--', spec.command, ...spec.args,
131
+ ],
132
+ removeArgv: ['mcp', 'remove', serverName],
133
+ }, serverName),
134
+ // ----- JSON config-file agents (safe deep-merge) -----
135
+ cursor: ({ spec, ctx: { home, serverName } }) => jsonPlan('cursor', path.join(home, '.cursor', 'mcp.json'), ['mcpServers'], serverName, jsonEntry(spec, { type: 'stdio' })),
136
+ // GitHub Copilot CLI: ~/.copilot/mcp-config.json, type "local".
137
+ // (VS Code Copilot uses a different `servers` key in
138
+ // .vscode/mcp.json; the terminal installer targets the CLI.)
139
+ copilot: ({ spec, ctx: { home, serverName } }) => jsonPlan('copilot', path.join(home, '.copilot', 'mcp-config.json'), ['mcpServers'], serverName, jsonEntry(spec, { type: 'local', tools: ['*'] })),
140
+ cline: ({ spec, ctx: { home, platform, serverName }, appData }) => jsonPlan('cline', clineConfigPath(home, platform, appData), ['mcpServers'], serverName, jsonEntry(spec, { disabled: false, autoApprove: [] })),
141
+ // OpenCode nests under `mcp`, folds command+args into one array,
142
+ // and gates the server with `enabled`.
143
+ opencode: ({ spec, ctx: { home, serverName } }) => jsonPlan('opencode', path.join(home, '.config', 'opencode', 'opencode.json'), ['mcp'], serverName, withEnv({ type: 'local', command: [spec.command, ...spec.args], enabled: true }, spec.env, 'environment')),
144
+ openclaw: ({ spec, ctx: { home, serverName } }) => jsonPlan('openclaw', path.join(home, '.openclaw', 'openclaw.json'), ['mcp', 'servers'], serverName, jsonEntry(spec)),
145
+ antigravity: ({ spec, ctx: { home, serverName } }) => jsonPlan('antigravity', path.join(home, '.gemini', 'antigravity', 'mcp_config.json'), ['mcpServers'], serverName, jsonEntry(spec)),
146
+ trae: ({ spec, ctx: { home, platform, serverName }, appData }) => jsonPlan('trae', traeConfigPath(home, platform, appData), ['mcpServers'], serverName, jsonEntry(spec)),
147
+ // ----- Unverified formats: print-only, never write -----
148
+ vibe: ({ spec, ctx: { home, serverName } }) => ({
149
+ kind: 'manual',
150
+ slug: 'vibe',
151
+ format: 'toml',
152
+ configPath: path.join(home, '.vibe', 'config.toml'),
153
+ snippet: vibeTomlSnippet(spec, serverName),
154
+ reason: 'Vibe uses a TOML array-of-tables ([[mcp_servers]]); ' +
155
+ 'its exact schema is unverified, so append this block by hand ' +
156
+ 'to avoid corrupting an existing config.',
157
+ }),
158
+ pi: ({ spec, ctx: { home, serverName } }) => ({
159
+ kind: 'manual',
160
+ slug: 'pi',
161
+ format: 'json',
162
+ configPath: path.join(home, '.pi', 'agent', 'mcp.json'),
163
+ snippet: JSON.stringify({ mcpServers: { [serverName]: jsonEntry(spec) } }, null, 2),
164
+ reason: 'The pi coding agent exposes MCP, but its config path/schema is ' +
165
+ 'not authoritatively documented. Paste this into pi’s MCP ' +
166
+ 'config (check `pi --help` for the exact location).',
167
+ }),
168
+ hermes: ({ spec, ctx: { home, serverName } }) => ({
169
+ kind: 'manual',
170
+ slug: 'hermes',
171
+ format: 'yaml',
172
+ configPath: path.join(home, '.hermes', 'config.yaml'),
173
+ snippet: hermesYamlSnippet(spec, serverName),
174
+ reason: 'Hermes config format is unverified. Add this under your Hermes ' +
175
+ 'MCP server configuration by hand.',
176
+ }),
177
+ };
101
178
  /**
102
179
  * Build the registration plan for one agent. Pure — all IO happens in the
103
180
  * executor. Throws on an unknown slug so callers surface a clear error.
104
181
  */
105
182
  export function planAgentInstall({ slug, spec, ctx }, { env = process.env } = {}) {
106
- const { home, platform, serverName } = ctx;
107
- switch (slug) {
108
- // ----- CLI-driven agents (idempotent via the agent's own tool) -----
109
- case 'claude':
110
- return {
111
- kind: 'cli',
112
- slug,
113
- bin: 'claude',
114
- addArgv: [
115
- 'mcp', 'add', '--scope', 'user',
116
- serverName,
117
- ...envFlags(spec.env, '-e'),
118
- '--', spec.command, ...spec.args,
119
- ],
120
- removeArgv: ['mcp', 'remove', '--scope', 'user', serverName],
121
- getArgv: ['mcp', 'get', serverName],
122
- };
123
- case 'codex':
124
- return {
125
- kind: 'cli',
126
- slug,
127
- bin: 'codex',
128
- addArgv: [
129
- 'mcp', 'add', serverName,
130
- ...envFlags(spec.env, '--env'),
131
- '--', spec.command, ...spec.args,
132
- ],
133
- removeArgv: ['mcp', 'remove', serverName],
134
- getArgv: ['mcp', 'get', serverName],
135
- };
136
- case 'kimi':
137
- return {
138
- kind: 'cli',
139
- slug,
140
- bin: 'kimi',
141
- addArgv: [
142
- 'mcp', 'add', '--transport', 'stdio',
143
- ...envFlags(spec.env, '--env'),
144
- serverName, '--', spec.command, ...spec.args,
145
- ],
146
- removeArgv: ['mcp', 'remove', serverName],
147
- getArgv: ['mcp', 'get', serverName],
148
- };
149
- // ----- JSON config-file agents (safe deep-merge) -----
150
- case 'cursor':
151
- return {
152
- kind: 'json',
153
- slug,
154
- configPath: path.join(home, '.cursor', 'mcp.json'),
155
- keyPath: ['mcpServers'],
156
- serverKey: serverName,
157
- entry: jsonEntry(spec, { type: 'stdio' }),
158
- };
159
- case 'copilot':
160
- // GitHub Copilot CLI: ~/.copilot/mcp-config.json, type "local".
161
- // (VS Code Copilot uses a different `servers` key in
162
- // .vscode/mcp.json; the terminal installer targets the CLI.)
163
- return {
164
- kind: 'json',
165
- slug,
166
- configPath: path.join(home, '.copilot', 'mcp-config.json'),
167
- keyPath: ['mcpServers'],
168
- serverKey: serverName,
169
- entry: jsonEntry(spec, { type: 'local', tools: ['*'] }),
170
- };
171
- case 'cline':
172
- return {
173
- kind: 'json',
174
- slug,
175
- configPath: clineConfigPath(home, platform, env.APPDATA),
176
- keyPath: ['mcpServers'],
177
- serverKey: serverName,
178
- entry: jsonEntry(spec, { disabled: false, autoApprove: [] }),
179
- };
180
- case 'opencode':
181
- // OpenCode nests under `mcp`, folds command+args into one array,
182
- // and gates the server with `enabled`.
183
- return {
184
- kind: 'json',
185
- slug,
186
- configPath: path.join(home, '.config', 'opencode', 'opencode.json'),
187
- keyPath: ['mcp'],
188
- serverKey: serverName,
189
- entry: (() => {
190
- const e = {
191
- type: 'local',
192
- command: [spec.command, ...spec.args],
193
- enabled: true,
194
- };
195
- if (Object.keys(spec.env).length > 0)
196
- e.environment = spec.env;
197
- return e;
198
- })(),
199
- };
200
- case 'openclaw':
201
- return {
202
- kind: 'json',
203
- slug,
204
- configPath: path.join(home, '.openclaw', 'openclaw.json'),
205
- keyPath: ['mcp', 'servers'],
206
- serverKey: serverName,
207
- entry: jsonEntry(spec),
208
- };
209
- case 'antigravity':
210
- return {
211
- kind: 'json',
212
- slug,
213
- configPath: path.join(home, '.gemini', 'antigravity', 'mcp_config.json'),
214
- keyPath: ['mcpServers'],
215
- serverKey: serverName,
216
- entry: jsonEntry(spec),
217
- };
218
- case 'trae':
219
- return {
220
- kind: 'json',
221
- slug,
222
- configPath: traeConfigPath(home, platform, env.APPDATA),
223
- keyPath: ['mcpServers'],
224
- serverKey: serverName,
225
- entry: jsonEntry(spec),
226
- };
227
- // ----- Unverified formats: print-only, never write -----
228
- case 'vibe':
229
- return {
230
- kind: 'manual',
231
- slug,
232
- format: 'toml',
233
- configPath: path.join(home, '.vibe', 'config.toml'),
234
- snippet: vibeTomlSnippet(spec, serverName),
235
- reason: 'Vibe uses a TOML array-of-tables ([[mcp_servers]]); ' +
236
- 'its exact schema is unverified, so append this block by hand ' +
237
- 'to avoid corrupting an existing config.',
238
- };
239
- case 'pi':
240
- return {
241
- kind: 'manual',
242
- slug,
243
- format: 'json',
244
- configPath: path.join(home, '.pi', 'agent', 'mcp.json'),
245
- snippet: genericMcpServersSnippet(spec, serverName),
246
- reason: 'The pi coding agent exposes MCP, but its config path/schema is ' +
247
- 'not authoritatively documented. Paste this into pi’s MCP ' +
248
- 'config (check `pi --help` for the exact location).',
249
- };
250
- case 'hermes':
251
- return {
252
- kind: 'manual',
253
- slug,
254
- format: 'yaml',
255
- configPath: path.join(home, '.hermes', 'config.yaml'),
256
- snippet: hermesYamlSnippet(spec, serverName),
257
- reason: 'Hermes config format is unverified. Add this under your Hermes ' +
258
- 'MCP server configuration by hand.',
259
- };
260
- default: {
261
- const exhaustive = slug;
262
- throw new Error(`unknown agent slug: ${String(exhaustive)}`);
263
- }
183
+ // Own-property lookup only: a slug such as `constructor` or `__proto__` must not
184
+ // resolve to an inherited Object.prototype member and be called as a planner.
185
+ if (!Object.prototype.hasOwnProperty.call(AGENT_PLANNERS, slug)) {
186
+ throw new Error(`unknown agent slug: ${String(slug)}`);
264
187
  }
188
+ return AGENT_PLANNERS[slug]({ spec, ctx, appData: env.APPDATA });
189
+ }
190
+ // VS Code-family editors keep per-user state under one platform-specific root.
191
+ function editorUserDir(app, home, platform, suppliedAppData) {
192
+ if (platform === 'darwin')
193
+ return path.join(home, 'Library', 'Application Support', app, 'User');
194
+ if (platform === 'win32')
195
+ return path.join(suppliedAppData ?? path.join(home, 'AppData', 'Roaming'), app, 'User');
196
+ return path.join(home, '.config', app, 'User');
265
197
  }
266
198
  function clineConfigPath(home, platform, suppliedAppData) {
267
- const rel = path.join('globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json');
268
- if (platform === 'darwin') {
269
- return path.join(home, 'Library', 'Application Support', 'Code', 'User', rel);
270
- }
271
- if (platform === 'win32') {
272
- const appData = suppliedAppData ?? path.join(home, 'AppData', 'Roaming');
273
- return path.join(appData, 'Code', 'User', rel);
274
- }
275
- return path.join(home, '.config', 'Code', 'User', rel);
199
+ return path.join(editorUserDir('Code', home, platform, suppliedAppData), 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json');
276
200
  }
277
201
  function traeConfigPath(home, platform, suppliedAppData) {
278
- if (platform === 'darwin') {
279
- return path.join(home, 'Library', 'Application Support', 'Trae', 'User', 'mcp.json');
280
- }
281
- if (platform === 'win32') {
282
- const appData = suppliedAppData ?? path.join(home, 'AppData', 'Roaming');
283
- return path.join(appData, 'Trae', 'User', 'mcp.json');
284
- }
285
- return path.join(home, '.config', 'Trae', 'User', 'mcp.json');
202
+ return path.join(editorUserDir('Trae', home, platform, suppliedAppData), 'mcp.json');
286
203
  }
287
204
  // --- Pure JSON merge / removal (the heart of the 'json' strategy) -------
288
205
  // CR-007: `JsonInstallPlan` is a public shape and `applyJsonInstall` /
@@ -372,15 +289,6 @@ function parseJsonObject(text, where) {
372
289
  return parsed;
373
290
  }
374
291
  // --- Snippets for the manual (print-only) strategy ----------------------
375
- function genericMcpServersSnippet(spec, name) {
376
- const server = {
377
- command: spec.command,
378
- args: spec.args,
379
- };
380
- if (Object.keys(spec.env).length > 0)
381
- server.env = spec.env;
382
- return JSON.stringify({ mcpServers: { [name]: server } }, null, 2);
383
- }
384
292
  function hermesYamlSnippet(spec, name) {
385
293
  const lines = [
386
294
  'mcp_servers:',
@@ -67,6 +67,8 @@ export declare const DAEMON_TOKEN_ENV_VAR = "JINI_DAEMON_TOKEN";
67
67
  * policy that would make a given number correct, so it must not refuse to boot over one.
68
68
  */
69
69
  export declare const DELEGATED_TOOL_TIMEOUT_ENV_VAR = "JINI_DELEGATED_TOOL_TIMEOUT_MS";
70
+ /** JSON array of host-selected visible registry IDs; never sourced from diagnostic IDs. */
71
+ export declare const STARTUP_TOOL_IDS_ENV_VAR = "JINI_STARTUP_TOOL_IDS";
70
72
  export interface ServeDeps {
71
73
  /** @default process.env */
72
74
  readonly env?: NodeJS.ProcessEnv;
@@ -105,7 +107,8 @@ export interface McpDaemonUrlOptions {
105
107
  * has no meaningful default run to serve) plus `DAEMON_URL_ENV_VAR` resolution, then hosts
106
108
  * `RUN_TOOLS` (run-agnostic) alongside a fresh `execute_delegated_tool` def scoped to the resolved
107
109
  * `runId`, and `KERNEL_RESOURCES`. Resolves once the client disconnects (stdin EOF) or the
108
- * idle-exit window elapses (`createMcpToolServer`'s own default) — never bare-throws: every error
110
+ * idle-exit window elapses (`createMcpToolServer`'s own default). Enables describe publication and
111
+ * resolves startup descriptors before connecting stdio. Never bare-throws: every error
109
112
  * path below writes a message to `writeErr` and calls `exit(1)` instead.
110
113
  */
111
114
  export declare function serve(_requiredArgs: Record<string, never>, deps?: ServeDeps): Promise<void>;
package/dist/bin/serve.js CHANGED
@@ -3,10 +3,11 @@ import { fileURLToPath } from 'node:url';
3
3
  import { isLoopbackHostname } from '@jini-ai/platform/net';
4
4
  import { createMcpToolServer as createMcpToolServerDefault } from '../server/tool-server.js';
5
5
  import { RUN_TOOLS } from '../server/tools/run-tools.js';
6
- import { TOOL_CATALOG_TOOLS } from '../server/tools/tool-catalog-tools.js';
6
+ import { describeToolTool, TOOL_CATALOG_TOOLS } from '../server/tools/tool-catalog-tools.js';
7
7
  import { COMPONENT_CATALOG_TOOLS } from '../server/tools/component-catalog-tools.js';
8
8
  import { createExecuteDelegatedToolTool, createExecuteReadonlyDelegatedToolTool } from '../server/tools/delegated-tool.js';
9
9
  import { KERNEL_RESOURCES } from '../server/resources/active-resource.js';
10
+ import { createSiteGuideResource } from '../server/resources/site-guide-resource.js';
10
11
  /** The one run this process is scoped to for its entire lifetime. Set by the spawning daemon, never by the MCP client. */
11
12
  export const RUN_ID_ENV_VAR = 'JINI_RUN_ID';
12
13
  /** Checked (only) when resolving the daemon HTTP base URL — see `resolveDaemonUrl`'s own docs; matches the CLI entry point's identically-named constant. */
@@ -36,18 +37,26 @@ export const DAEMON_TOKEN_ENV_VAR = 'JINI_DAEMON_TOKEN';
36
37
  * policy that would make a given number correct, so it must not refuse to boot over one.
37
38
  */
38
39
  export const DELEGATED_TOOL_TIMEOUT_ENV_VAR = 'JINI_DELEGATED_TOOL_TIMEOUT_MS';
40
+ /** JSON array of host-selected visible registry IDs; never sourced from diagnostic IDs. */
41
+ export const STARTUP_TOOL_IDS_ENV_VAR = 'JINI_STARTUP_TOOL_IDS';
39
42
  const SERVER_NAME = 'jini-mcp';
40
43
  const SERVER_VERSION = '0.0.0';
41
44
  const INSTRUCTIONS = 'Tools proxying this host\'s Jini daemon over loopback HTTP, scoped to the run that spawned this MCP server process. ' +
42
45
  'Use execute_delegated_tool to invoke a Jini-registered tool (never an agent-vendor-specific tool name) against that run — ' +
43
- 'every call is routed through the daemon\'s deny-by-default ToolExecutor gate.';
46
+ 'every call is routed through the daemon\'s deny-by-default ToolExecutor gate. ' +
47
+ 'Concrete operation tools accept their own arguments directly. describe_tool publishes a concrete definition; ' +
48
+ 'refresh tools/list when supported, or use the delegated gateways as the compatibility fallback. ' +
49
+ 'When the host provides site_guide, consult jini://site-guide or call site_guide with operation overview ' +
50
+ 'through execute_readonly_delegated_tool; follow with explain for the relevant topic ids. ' +
51
+ 'If the host does not provide site_guide, report that availability gap.';
44
52
  // Keep the bin's environment-only assembly here; a general CLI resolver would introduce a
45
53
  // runtime dependency on another shell. No discovery/default URL is selected for a run child.
46
54
  async function resolveDaemonUrlDefault(_required, options) {
47
55
  const url = options.env[options.envVarName];
48
- if (!url)
49
- throw new Error('no daemon URL resolved: pass flagUrl, set envVarName on a populated env var, ' +
50
- 'supply a discover() probe, or provide defaultUrl.');
56
+ if (!url) {
57
+ throw new Error(`no daemon URL resolved: ${options.envVarName} is unset or empty; ` +
58
+ 'the spawning Jini daemon must set it to its HTTP base URL.');
59
+ }
51
60
  let parsed;
52
61
  try {
53
62
  parsed = new URL(url);
@@ -56,16 +65,18 @@ async function resolveDaemonUrlDefault(_required, options) {
56
65
  return url;
57
66
  }
58
67
  if (parsed.protocol !== 'https:' && !isLoopbackHostname({ hostname: parsed.hostname })) {
59
- let display = url;
60
- if (parsed.username || parsed.password) {
61
- parsed.username = '';
62
- parsed.password = '';
63
- display = parsed.toString();
64
- }
65
- options.warn({ message: `daemon URL ${display} is neither loopback nor HTTPS; traffic to a remote, non-HTTPS daemon is not encrypted.` });
68
+ options.warn({ message: `daemon URL ${redactUserinfo(url, parsed)} is neither loopback nor HTTPS; traffic to a remote, non-HTTPS daemon is not encrypted.` });
66
69
  }
67
70
  return url;
68
71
  }
72
+ // Keeps credentials out of the warning; a URL without userinfo is shown exactly as given.
73
+ function redactUserinfo(url, parsed) {
74
+ if (!parsed.username && !parsed.password)
75
+ return url;
76
+ parsed.username = '';
77
+ parsed.password = '';
78
+ return parsed.toString();
79
+ }
69
80
  function defaultWriteErr({ text }) {
70
81
  process.stderr.write(text);
71
82
  }
@@ -74,35 +85,76 @@ function defaultWriteErr({ text }) {
74
85
  * has no meaningful default run to serve) plus `DAEMON_URL_ENV_VAR` resolution, then hosts
75
86
  * `RUN_TOOLS` (run-agnostic) alongside a fresh `execute_delegated_tool` def scoped to the resolved
76
87
  * `runId`, and `KERNEL_RESOURCES`. Resolves once the client disconnects (stdin EOF) or the
77
- * idle-exit window elapses (`createMcpToolServer`'s own default) — never bare-throws: every error
88
+ * idle-exit window elapses (`createMcpToolServer`'s own default). Enables describe publication and
89
+ * resolves startup descriptors before connecting stdio. Never bare-throws: every error
78
90
  * path below writes a message to `writeErr` and calls `exit(1)` instead.
79
91
  */
80
92
  export async function serve(_requiredArgs, deps = {}) {
81
- const env = deps.env ?? process.env;
82
- const writeErr = deps.writeErr ?? defaultWriteErr;
83
- const exit = deps.exit ?? (({ code }) => process.exit(code));
84
- const resolveDaemonUrlFn = deps.resolveDaemonUrl ?? resolveDaemonUrlDefault;
85
- const createMcpToolServerFn = deps.createMcpToolServer ?? createMcpToolServerDefault;
93
+ const { env, writeErr, exit, resolveDaemonUrlFn, createMcpToolServerFn } = withProcessDefaults(deps);
86
94
  const runId = env[RUN_ID_ENV_VAR];
87
95
  if (typeof runId !== 'string' || runId.length === 0) {
88
96
  writeErr({ text: `${SERVER_NAME}: missing required ${RUN_ID_ENV_VAR} environment variable (this process must be spawned by a Jini daemon, which sets it)\n` });
89
97
  return exit({ code: 1 });
90
98
  }
99
+ let baseUrlResolution;
91
100
  const resolveBaseUrl = () => {
92
101
  const options = { flagUrl: null, env, envVarName: DAEMON_URL_ENV_VAR, warn: ({ message }) => writeErr({ text: message }) };
93
- return resolveDaemonUrlFn({}, options);
102
+ // Startup discovery and subsequent execution must use the same resolved daemon.
103
+ return baseUrlResolution ??= resolveDaemonUrlFn({}, options);
104
+ };
105
+ const delegatedGatewayOptions = buildDelegatedGatewayOptions({ env }, deps);
106
+ const requiredArgs = {
107
+ name: SERVER_NAME,
108
+ version: SERVER_VERSION,
109
+ tools: buildTools({ runId, delegatedGatewayOptions }),
110
+ resolveBaseUrl,
111
+ };
112
+ const serverOptions = buildServerOptions({ env, runId, delegatedGatewayOptions }, deps);
113
+ try {
114
+ const diagnosticToolIds = parseLaunchToolIds({ env, key: 'JINI_DIAGNOSTIC_TOOL_IDS' });
115
+ if (diagnosticToolIds.length)
116
+ Object.assign(serverOptions, { diagnosticToolIds });
117
+ const initialTools = await resolveStartupTools({
118
+ toolIds: parseLaunchToolIds({ env, key: STARTUP_TOOL_IDS_ENV_VAR }), resolveBaseUrl, serverOptions,
119
+ });
120
+ Object.assign(serverOptions, { delegatedTools: { runId, ...delegatedGatewayOptions, initialTools } });
121
+ await createMcpToolServerFn(requiredArgs, serverOptions).run({});
122
+ }
123
+ catch (err) {
124
+ const message = err instanceof Error ? err.message : String(err);
125
+ writeErr({ text: `${SERVER_NAME}: ${message}\n` });
126
+ return exit({ code: 1 });
127
+ }
128
+ }
129
+ // The process-bound fallbacks for every injectable seam `serve()` itself calls.
130
+ function withProcessDefaults(deps) {
131
+ return {
132
+ env: deps.env ?? process.env,
133
+ writeErr: deps.writeErr ?? defaultWriteErr,
134
+ exit: deps.exit ?? (({ code }) => process.exit(code)),
135
+ resolveDaemonUrlFn: deps.resolveDaemonUrl ?? resolveDaemonUrlDefault,
136
+ createMcpToolServerFn: deps.createMcpToolServer ?? createMcpToolServerDefault,
94
137
  };
95
- // Shared by both gateways so their run scope, correlation-id source, and deadline cannot diverge
96
- // — the only difference between the two defs is the read-only constraint each declares.
97
- const delegatedGatewayOptions = {
98
- ...(deps.generateToolUseId !== undefined ? { generateToolUseId: deps.generateToolUseId } : {}),
138
+ }
139
+ // Drops `undefined` values so an absent dependency leaves no key at all — required under
140
+ // `exactOptionalPropertyTypes`, and it keeps built option objects byte-identical to the
141
+ // pre-injection shape when nothing is overridden.
142
+ function definedOnly(entries) {
143
+ return Object.fromEntries(Object.entries(entries).filter(([, value]) => value !== undefined));
144
+ }
145
+ // Shared by both gateways so their run scope, correlation-id source, and deadline cannot diverge
146
+ // — the only difference between the two defs is the read-only constraint each declares.
147
+ function buildDelegatedGatewayOptions({ env }, deps) {
148
+ const rawTimeout = env[DELEGATED_TOOL_TIMEOUT_ENV_VAR];
149
+ return definedOnly({
150
+ generateToolUseId: deps.generateToolUseId,
99
151
  // Parsed, not validated: `createExecuteDelegatedToolTool` already rejects a non-finite or
100
152
  // non-positive value back to the default, so `NaN` from a malformed env var lands there too.
101
- ...(env[DELEGATED_TOOL_TIMEOUT_ENV_VAR] !== undefined
102
- ? { delegatedToolTimeoutMs: Number(env[DELEGATED_TOOL_TIMEOUT_ENV_VAR]) }
103
- : {}),
104
- };
105
- const tools = [
153
+ delegatedToolTimeoutMs: rawTimeout === undefined ? undefined : Number(rawTimeout),
154
+ });
155
+ }
156
+ function buildTools({ runId, delegatedGatewayOptions }) {
157
+ return [
106
158
  ...RUN_TOOLS,
107
159
  ...TOOL_CATALOG_TOOLS,
108
160
  ...COMPONENT_CATALOG_TOOLS,
@@ -112,29 +164,52 @@ export async function serve(_requiredArgs, deps = {}) {
112
164
  // sees both and picks by what its call actually does.
113
165
  createExecuteReadonlyDelegatedToolTool({ runId }, delegatedGatewayOptions),
114
166
  ];
167
+ }
168
+ function buildServerOptions({ env, runId, delegatedGatewayOptions }, deps) {
115
169
  // Absent credential => no `authHeaders` at all => byte-identical request headers to before this
116
170
  // env var existed. See DAEMON_TOKEN_ENV_VAR's doc for why an unset value is not a startup failure.
117
171
  const credential = env[DAEMON_TOKEN_ENV_VAR];
118
- const authHeaders = typeof credential === 'string' && credential.length > 0
119
- ? { Authorization: `Bearer ${credential}` }
120
- : undefined;
121
- const requiredArgs = { name: SERVER_NAME, version: SERVER_VERSION, tools, resolveBaseUrl };
122
- const serverOptions = {
123
- resources: KERNEL_RESOURCES,
172
+ return {
173
+ resources: [...KERNEL_RESOURCES, createSiteGuideResource({ runId }, delegatedGatewayOptions)],
124
174
  instructions: INSTRUCTIONS,
125
- ...(authHeaders !== undefined ? { authHeaders } : {}),
126
- ...(deps.fetchImpl !== undefined ? { fetchImpl: deps.fetchImpl } : {}),
127
- ...(deps.stdin !== undefined ? { stdin: deps.stdin } : {}),
128
- ...(deps.stdout !== undefined ? { stdout: deps.stdout } : {}),
175
+ ...definedOnly({
176
+ authHeaders: credential ? { Authorization: `Bearer ${credential}` } : undefined,
177
+ fetchImpl: deps.fetchImpl,
178
+ stdin: deps.stdin,
179
+ stdout: deps.stdout,
180
+ }),
129
181
  };
130
- try {
131
- await createMcpToolServerFn(requiredArgs, serverOptions).run({});
132
- }
133
- catch (err) {
134
- const message = err instanceof Error ? err.message : String(err);
135
- writeErr({ text: `${SERVER_NAME}: ${message}\n` });
136
- return exit({ code: 1 });
182
+ }
183
+ // Only the spawning host supplies registration metadata. Never learn exceptions from an
184
+ // error body or model arguments: either would turn a secret into its own redaction exemption.
185
+ function parseLaunchToolIds({ env, key }) {
186
+ const toolIds = JSON.parse(env[key] ?? '[]');
187
+ if (!Array.isArray(toolIds) || !toolIds.every(id => typeof id === 'string')) {
188
+ throw new Error(`${key} must be a JSON array of registered tool IDs`);
137
189
  }
190
+ return toolIds;
191
+ }
192
+ /**
193
+ * Resolve each unique startup ID through the existing authenticated describe/publication owner.
194
+ * The temporary descriptor list seeds M1's bounded exposure; it is not an execution catalog.
195
+ * Failure aborts startup before any partially resolved list can reach the client.
196
+ * @returns Authoritative descriptors; no daemon I/O when startup IDs are absent.
197
+ * @throws The existing descriptor/daemon errors, reported by serve's stderr/exit boundary.
198
+ * @complexity O(n + s) space/time plus n sequential bounded GETs for n unique IDs and s schema bytes.
199
+ */
200
+ async function resolveStartupTools({ toolIds, resolveBaseUrl, serverOptions }) {
201
+ if (toolIds.length === 0)
202
+ return [];
203
+ const descriptors = [];
204
+ const ctx = {
205
+ baseUrl: await resolveBaseUrl(),
206
+ fetchImpl: serverOptions.fetchImpl ?? fetch,
207
+ ...definedOnly({ authHeaders: serverOptions.authHeaders, diagnosticToolIds: serverOptions.diagnosticToolIds }),
208
+ publishTool: async ({ descriptor }) => { descriptors.push(descriptor); },
209
+ };
210
+ for (const id of new Set(toolIds))
211
+ await describeToolTool.handler({ args: { id }, ctx });
212
+ return descriptors;
138
213
  }
139
214
  // Only run for real when this module is the actual process entrypoint (`node dist/bin/serve.js`,
140
215
  // or the `jini-mcp` bin shim pnpm/npm links to it) — not merely imported, e.g. by this file's own