@jini-ai/mcp 0.4.3 → 0.6.10
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 +76 -0
- package/dist/agent-install/install.js +102 -194
- package/dist/bin/serve.d.ts +4 -1
- package/dist/bin/serve.js +115 -43
- 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/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 +71 -36
- package/dist/server/daemon-client.d.ts +2 -0
- package/dist/server/daemon-client.js +86 -69
- 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 +14 -0
- package/dist/server/tools/delegated-tool.js +43 -4
- 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 +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
# @jini-ai/mcp
|
|
2
2
|
|
|
3
|
+
## 0.6.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4f52784: Add a stable `./bin` export subpath resolving to the `jini-mcp` executable.
|
|
8
|
+
|
|
9
|
+
The package declared a `bin` entry for `dist/bin/serve.js` but an `exports` map containing only
|
|
10
|
+
`"."`, so a consumer that needed the executable's path in order to spawn it as a CLI-injected MCP
|
|
11
|
+
server could not ask for one: `require.resolve('@jini-ai/mcp/dist/bin/serve.js')` fails with
|
|
12
|
+
`ERR_PACKAGE_PATH_NOT_EXPORTED`. Consumers were resolving the root export and walking to a sibling
|
|
13
|
+
path by hand, hardcoding this package's private build layout.
|
|
14
|
+
|
|
15
|
+
`require.resolve('@jini-ai/mcp/bin')` now returns that path directly. The subpath deliberately
|
|
16
|
+
declares only a `default` condition — it is meant to be resolved to a path and handed to `spawn`,
|
|
17
|
+
not imported (`serve.ts` uses top-level await behind its `isMainModule` guard, so it is not
|
|
18
|
+
`require()`-able). Purely additive: the `"."` export is unchanged.
|
|
19
|
+
|
|
20
|
+
- 8ff5653: Enforce a tool's declared `inputSchema` server-side before its handler runs.
|
|
21
|
+
|
|
22
|
+
`createMcpToolServer` wires tools through the MCP SDK's low-level `Server.setRequestHandler`, not
|
|
23
|
+
the schema-validating `McpServer.registerTool()` helper — the former never checked a real client's
|
|
24
|
+
`tools/call` arguments against the tool's own declared `inputSchema`. A wire-level test (a real SDK
|
|
25
|
+
`Client` and `Server` over `InMemoryTransport`, not the existing `FakeTransport` suite, which calls
|
|
26
|
+
handlers directly either way and so cannot see this) proved schema-violating arguments — wrong
|
|
27
|
+
type, an undeclared property under `additionalProperties: false` — reached every handler completely
|
|
28
|
+
unvalidated.
|
|
29
|
+
|
|
30
|
+
`handleToolCall` now validates `rawArgs` against `tool.inputSchema` via the MCP SDK's own
|
|
31
|
+
`@modelcontextprotocol/sdk/validation/cfworker` provider (no `eval`/codegen, matching this package's
|
|
32
|
+
Node CLI runtime) before invoking the handler, returning the same `{isError:true}` shape as an
|
|
33
|
+
unknown-tool-name or thrown-error result rather than ever calling the handler with unvalidated
|
|
34
|
+
input. Each tool's compiled validator is cached (`WeakMap` keyed by the `McpToolDef` instance, stable
|
|
35
|
+
for a server's whole lifetime) since compiling one is real work the SDK's validator does not cache
|
|
36
|
+
internally. Adds `@cfworker/json-schema` (already an optional peer of the installed SDK version) as
|
|
37
|
+
a direct dependency.
|
|
38
|
+
|
|
39
|
+
- 3fe4d14: Add default-off delegated-result v1 text markers for generic, read-only and typed gateways. The stdio host can opt in with JINI_DELEGATED_RESULT_MARKERS=1.
|
|
40
|
+
- 9098d22: Describe site-guide discovery as an optional host capability in the generic MCP instructions.
|
|
41
|
+
- 4f52784: Add a `sidecar-strict` security mode and per-run MCP credential propagation.
|
|
42
|
+
|
|
43
|
+
For a daemon whose threat model is **another process running as the same OS user** rather than a
|
|
44
|
+
remote attacker, the existing `jini-local` mode is a no-op: `registerApiBearerAuthMiddleware`
|
|
45
|
+
short-circuits for any loopback peer before it reads the `Authorization` header, and a `127.0.0.1`
|
|
46
|
+
bind keeps remote hosts out while doing nothing about a co-resident process. A consumer that spawns a
|
|
47
|
+
Jini daemon holding real authority — starting agent runs, executing tools against a real database —
|
|
48
|
+
previously had to write its own middleware.
|
|
49
|
+
|
|
50
|
+
- `@jini-ai/http-kit` gains `requireStrictBearerToken`: fail-closed 503 when the named token env var
|
|
51
|
+
is unset, 401 on mismatch, **no loopback exemption and no disable flag**. Its `tokenEnvVar` is
|
|
52
|
+
required with no default, so this package never names a host's secret.
|
|
53
|
+
- `composeJiniKernel` gains `security: { mode: 'sidecar-strict', host, tokenEnvVar, exemptPaths? }`.
|
|
54
|
+
Purely additive — `host` and `jini-local` are unchanged by construction, since the modes are arms of
|
|
55
|
+
a discriminated union. The strict gate mounts ahead of the JSON body parser, so a caller it rejects
|
|
56
|
+
never has its body parsed.
|
|
57
|
+
- `@jini-ai/daemon`'s `McpJsonInjectionOptions` gains `credential?: (runId) => string | Promise<string>`
|
|
58
|
+
— a **resolver, not a string**, because injection options are built once before any run exists and a
|
|
59
|
+
boot-wide shared secret would defeat the point of scoping a credential to a run. It is delivered to
|
|
60
|
+
the child as `JINI_DAEMON_TOKEN`.
|
|
61
|
+
- `@jini-ai/mcp`'s `jini-mcp` reads that variable and attaches `Authorization` to every daemon call.
|
|
62
|
+
Optional throughout: with no credential, request headers and `.mcp.json` output are byte-identical
|
|
63
|
+
to before.
|
|
64
|
+
|
|
65
|
+
Also generalized: both existing bearer gates now compare tokens in constant time (`timingSafeEqual`)
|
|
66
|
+
and share one header-parsing helper, closing a timing side channel and removing a duplicated regex.
|
|
67
|
+
|
|
68
|
+
- Updated dependencies [22f0619]
|
|
69
|
+
- Updated dependencies [82ed2d3]
|
|
70
|
+
- Updated dependencies [3fe4d14]
|
|
71
|
+
- Updated dependencies [5a3de86]
|
|
72
|
+
- @jini-ai/core@0.6.10
|
|
73
|
+
- @jini-ai/protocol@0.6.10
|
|
74
|
+
|
|
75
|
+
## 0.4.4 — 2026-10-22
|
|
76
|
+
|
|
77
|
+
- 2026-10-22: Typed tools from the launched bridge, concrete describe schemas, list-changed, diagnostic error codes, coverage.
|
|
78
|
+
|
|
3
79
|
## 0.4.2
|
|
4
80
|
|
|
5
81
|
### Patch Changes
|
|
@@ -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,7 +3,7 @@ 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';
|
|
@@ -37,11 +37,15 @@ export const DAEMON_TOKEN_ENV_VAR = 'JINI_DAEMON_TOKEN';
|
|
|
37
37
|
* policy that would make a given number correct, so it must not refuse to boot over one.
|
|
38
38
|
*/
|
|
39
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';
|
|
40
42
|
const SERVER_NAME = 'jini-mcp';
|
|
41
43
|
const SERVER_VERSION = '0.0.0';
|
|
42
44
|
const INSTRUCTIONS = 'Tools proxying this host\'s Jini daemon over loopback HTTP, scoped to the run that spawned this MCP server process. ' +
|
|
43
45
|
'Use execute_delegated_tool to invoke a Jini-registered tool (never an agent-vendor-specific tool name) against that run — ' +
|
|
44
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. ' +
|
|
45
49
|
'When the host provides site_guide, consult jini://site-guide or call site_guide with operation overview ' +
|
|
46
50
|
'through execute_readonly_delegated_tool; follow with explain for the relevant topic ids. ' +
|
|
47
51
|
'If the host does not provide site_guide, report that availability gap.';
|
|
@@ -49,9 +53,10 @@ const INSTRUCTIONS = 'Tools proxying this host\'s Jini daemon over loopback HTTP
|
|
|
49
53
|
// runtime dependency on another shell. No discovery/default URL is selected for a run child.
|
|
50
54
|
async function resolveDaemonUrlDefault(_required, options) {
|
|
51
55
|
const url = options.env[options.envVarName];
|
|
52
|
-
if (!url)
|
|
53
|
-
throw new Error(
|
|
54
|
-
'
|
|
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
|
+
}
|
|
55
60
|
let parsed;
|
|
56
61
|
try {
|
|
57
62
|
parsed = new URL(url);
|
|
@@ -60,16 +65,18 @@ async function resolveDaemonUrlDefault(_required, options) {
|
|
|
60
65
|
return url;
|
|
61
66
|
}
|
|
62
67
|
if (parsed.protocol !== 'https:' && !isLoopbackHostname({ hostname: parsed.hostname })) {
|
|
63
|
-
|
|
64
|
-
if (parsed.username || parsed.password) {
|
|
65
|
-
parsed.username = '';
|
|
66
|
-
parsed.password = '';
|
|
67
|
-
display = parsed.toString();
|
|
68
|
-
}
|
|
69
|
-
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.` });
|
|
70
69
|
}
|
|
71
70
|
return url;
|
|
72
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
|
+
}
|
|
73
80
|
function defaultWriteErr({ text }) {
|
|
74
81
|
process.stderr.write(text);
|
|
75
82
|
}
|
|
@@ -78,35 +85,77 @@ function defaultWriteErr({ text }) {
|
|
|
78
85
|
* has no meaningful default run to serve) plus `DAEMON_URL_ENV_VAR` resolution, then hosts
|
|
79
86
|
* `RUN_TOOLS` (run-agnostic) alongside a fresh `execute_delegated_tool` def scoped to the resolved
|
|
80
87
|
* `runId`, and `KERNEL_RESOURCES`. Resolves once the client disconnects (stdin EOF) or the
|
|
81
|
-
* 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
|
|
82
90
|
* path below writes a message to `writeErr` and calls `exit(1)` instead.
|
|
83
91
|
*/
|
|
84
92
|
export async function serve(_requiredArgs, deps = {}) {
|
|
85
|
-
const env = deps
|
|
86
|
-
const writeErr = deps.writeErr ?? defaultWriteErr;
|
|
87
|
-
const exit = deps.exit ?? (({ code }) => process.exit(code));
|
|
88
|
-
const resolveDaemonUrlFn = deps.resolveDaemonUrl ?? resolveDaemonUrlDefault;
|
|
89
|
-
const createMcpToolServerFn = deps.createMcpToolServer ?? createMcpToolServerDefault;
|
|
93
|
+
const { env, writeErr, exit, resolveDaemonUrlFn, createMcpToolServerFn } = withProcessDefaults(deps);
|
|
90
94
|
const runId = env[RUN_ID_ENV_VAR];
|
|
91
95
|
if (typeof runId !== 'string' || runId.length === 0) {
|
|
92
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` });
|
|
93
97
|
return exit({ code: 1 });
|
|
94
98
|
}
|
|
99
|
+
let baseUrlResolution;
|
|
95
100
|
const resolveBaseUrl = () => {
|
|
96
101
|
const options = { flagUrl: null, env, envVarName: DAEMON_URL_ENV_VAR, warn: ({ message }) => writeErr({ text: message }) };
|
|
97
|
-
|
|
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,
|
|
98
137
|
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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,
|
|
151
|
+
delegatedResultMarkers: env.JINI_DELEGATED_RESULT_MARKERS === '1' ? true : undefined,
|
|
103
152
|
// Parsed, not validated: `createExecuteDelegatedToolTool` already rejects a non-finite or
|
|
104
153
|
// non-positive value back to the default, so `NaN` from a malformed env var lands there too.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
154
|
+
delegatedToolTimeoutMs: rawTimeout === undefined ? undefined : Number(rawTimeout),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function buildTools({ runId, delegatedGatewayOptions }) {
|
|
158
|
+
return [
|
|
110
159
|
...RUN_TOOLS,
|
|
111
160
|
...TOOL_CATALOG_TOOLS,
|
|
112
161
|
...COMPONENT_CATALOG_TOOLS,
|
|
@@ -116,29 +165,52 @@ export async function serve(_requiredArgs, deps = {}) {
|
|
|
116
165
|
// sees both and picks by what its call actually does.
|
|
117
166
|
createExecuteReadonlyDelegatedToolTool({ runId }, delegatedGatewayOptions),
|
|
118
167
|
];
|
|
168
|
+
}
|
|
169
|
+
function buildServerOptions({ env, runId, delegatedGatewayOptions }, deps) {
|
|
119
170
|
// Absent credential => no `authHeaders` at all => byte-identical request headers to before this
|
|
120
171
|
// env var existed. See DAEMON_TOKEN_ENV_VAR's doc for why an unset value is not a startup failure.
|
|
121
172
|
const credential = env[DAEMON_TOKEN_ENV_VAR];
|
|
122
|
-
|
|
123
|
-
? { Authorization: `Bearer ${credential}` }
|
|
124
|
-
: undefined;
|
|
125
|
-
const requiredArgs = { name: SERVER_NAME, version: SERVER_VERSION, tools, resolveBaseUrl };
|
|
126
|
-
const serverOptions = {
|
|
173
|
+
return {
|
|
127
174
|
resources: [...KERNEL_RESOURCES, createSiteGuideResource({ runId }, delegatedGatewayOptions)],
|
|
128
175
|
instructions: INSTRUCTIONS,
|
|
129
|
-
...(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
176
|
+
...definedOnly({
|
|
177
|
+
authHeaders: credential ? { Authorization: `Bearer ${credential}` } : undefined,
|
|
178
|
+
fetchImpl: deps.fetchImpl,
|
|
179
|
+
stdin: deps.stdin,
|
|
180
|
+
stdout: deps.stdout,
|
|
181
|
+
}),
|
|
133
182
|
};
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
183
|
+
}
|
|
184
|
+
// Only the spawning host supplies registration metadata. Never learn exceptions from an
|
|
185
|
+
// error body or model arguments: either would turn a secret into its own redaction exemption.
|
|
186
|
+
function parseLaunchToolIds({ env, key }) {
|
|
187
|
+
const toolIds = JSON.parse(env[key] ?? '[]');
|
|
188
|
+
if (!Array.isArray(toolIds) || !toolIds.every(id => typeof id === 'string')) {
|
|
189
|
+
throw new Error(`${key} must be a JSON array of registered tool IDs`);
|
|
141
190
|
}
|
|
191
|
+
return toolIds;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Resolve each unique startup ID through the existing authenticated describe/publication owner.
|
|
195
|
+
* The temporary descriptor list seeds M1's bounded exposure; it is not an execution catalog.
|
|
196
|
+
* Failure aborts startup before any partially resolved list can reach the client.
|
|
197
|
+
* @returns Authoritative descriptors; no daemon I/O when startup IDs are absent.
|
|
198
|
+
* @throws The existing descriptor/daemon errors, reported by serve's stderr/exit boundary.
|
|
199
|
+
* @complexity O(n + s) space/time plus n sequential bounded GETs for n unique IDs and s schema bytes.
|
|
200
|
+
*/
|
|
201
|
+
async function resolveStartupTools({ toolIds, resolveBaseUrl, serverOptions }) {
|
|
202
|
+
if (toolIds.length === 0)
|
|
203
|
+
return [];
|
|
204
|
+
const descriptors = [];
|
|
205
|
+
const ctx = {
|
|
206
|
+
baseUrl: await resolveBaseUrl(),
|
|
207
|
+
fetchImpl: serverOptions.fetchImpl ?? fetch,
|
|
208
|
+
...definedOnly({ authHeaders: serverOptions.authHeaders, diagnosticToolIds: serverOptions.diagnosticToolIds }),
|
|
209
|
+
publishTool: async ({ descriptor }) => { descriptors.push(descriptor); },
|
|
210
|
+
};
|
|
211
|
+
for (const id of new Set(toolIds))
|
|
212
|
+
await describeToolTool.handler({ args: { id }, ctx });
|
|
213
|
+
return descriptors;
|
|
142
214
|
}
|
|
143
215
|
// Only run for real when this module is the actual process entrypoint (`node dist/bin/serve.js`,
|
|
144
216
|
// or the `jini-mcp` bin shim pnpm/npm links to it) — not merely imported, e.g. by this file's own
|