@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.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -0
- package/dist/agent-install/chrome-devtools.d.ts +4 -0
- package/dist/agent-install/chrome-devtools.js +7 -0
- package/dist/agent-install/index.d.ts +1 -0
- package/dist/agent-install/index.js +1 -0
- package/dist/agent-install/install.js +102 -194
- package/dist/bin/serve.d.ts +4 -1
- package/dist/bin/serve.js +120 -45
- package/dist/client/client.js +71 -58
- package/dist/core/config.js +113 -149
- package/dist/core/tokens.js +29 -53
- package/dist/federation/adapter.http.js +3 -2
- package/dist/federation/approvals/call-confirmation.js +5 -70
- package/dist/federation/approvals/remembered-approval.d.ts +39 -0
- package/dist/federation/approvals/remembered-approval.js +98 -0
- package/dist/federation/approvals/revocation-reason.d.ts +9 -0
- package/dist/federation/approvals/revocation-reason.js +11 -0
- package/dist/federation/approvals/revocation.js +19 -13
- package/dist/federation/approvals/scope-option.d.ts +6 -0
- package/dist/federation/approvals/scope-option.js +6 -0
- package/dist/federation/bootstrap.d.ts +18 -0
- package/dist/federation/bootstrap.js +4 -4
- package/dist/federation/presets.d.ts +16 -17
- package/dist/federation/presets.js +35 -28
- package/dist/federation/stdio/adapter.stdio.js +14 -6
- package/dist/federation/stdio/default-connect.js +24 -27
- package/dist/federation/stdio/stdio-launch-resolver.d.ts +1 -1
- package/dist/federation/stdio/stdio-launch-resolver.js +13 -9
- package/dist/federation/tool-approvals.js +3 -1
- package/dist/federation/trust.d.ts +23 -2
- package/dist/federation/trust.js +69 -35
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/server/daemon-client.d.ts +2 -0
- package/dist/server/daemon-client.js +86 -69
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/resources/site-guide-resource.d.ts +15 -0
- package/dist/server/resources/site-guide-resource.js +25 -0
- package/dist/server/tool-arguments.d.ts +46 -0
- package/dist/server/tool-arguments.js +111 -0
- package/dist/server/tool-protocol.d.ts +62 -7
- package/dist/server/tool-protocol.js +30 -40
- package/dist/server/tool-server.d.ts +19 -0
- package/dist/server/tool-server.js +91 -19
- package/dist/server/tools/delegated-tool.d.ts +47 -0
- package/dist/server/tools/delegated-tool.js +35 -2
- package/dist/server/tools/tool-catalog-tools.js +30 -8
- package/dist/tools/ask-choice/index.d.ts +213 -14
- package/dist/tools/ask-choice/index.js +90 -46
- package/dist/tools/ask-choice/question.d.ts +5 -1
- package/dist/tools/ask-choice/question.js +36 -2
- package/dist/tools/ask-choice/schema.d.ts +196 -10
- package/dist/tools/ask-choice/schema.js +33 -12
- package/dist/tools/ask-choice/types.d.ts +8 -0
- package/package.json +6 -5
- package/dist/core/oauth.d.ts +0 -274
- package/dist/core/oauth.js +0 -667
- package/dist/core/secure-write.d.ts +0 -17
- package/dist/core/secure-write.js +0 -72
package/CHANGELOG.md
CHANGED
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,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
|
|
@@ -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 = {}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:',
|
package/dist/bin/serve.d.ts
CHANGED
|
@@ -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)
|
|
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(
|
|
50
|
-
'
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
...(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|