@jini-ai/mcp 0.6.17 → 0.6.19
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 +5 -0
- package/dist/bin/serve.js +12 -81
- package/dist/core/config.js +3 -1
- package/dist/core/tokens.js +3 -1
- package/dist/federation/bootstrap.d.ts +3 -4
- package/dist/federation/bootstrap.js +11 -3
- package/dist/federation/reload.d.ts +6 -1
- package/dist/federation/reload.js +24 -5
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/server/daemon-client.d.ts +2 -2
- package/dist/server/daemon-client.js +4 -1
- package/dist/server/http-tool-server.d.ts +44 -0
- package/dist/server/http-tool-server.js +272 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +2 -0
- package/dist/server/run-tool-host.d.ts +28 -0
- package/dist/server/run-tool-host.js +103 -0
- package/dist/server/sdk-transport.d.ts +7 -1
- package/dist/server/tool-arguments.d.ts +28 -4
- package/dist/server/tool-arguments.js +47 -9
- package/dist/server/tool-protocol.js +39 -9
- package/dist/server/tool-result-pages.d.ts +21 -0
- package/dist/server/tool-result-pages.js +74 -0
- package/dist/server/tool-server.d.ts +11 -5
- package/dist/server/tool-server.js +78 -53
- package/dist/server/tools/delegated-tool.d.ts +7 -6
- package/dist/server/tools/delegated-tool.js +8 -3
- package/dist/server/tools/wait.d.ts +5 -0
- package/dist/server/tools/wait.js +17 -0
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @jini-ai/mcp
|
|
2
2
|
|
|
3
|
+
## 0.6.19
|
|
4
|
+
|
|
5
|
+
- Redact config/token parsing and federation bootstrap exception diagnostics.
|
|
6
|
+
- A top-level `integer` argument sent as a canonical integer string (`search_tools {max_results: "8"}`) is read as that integer instead of refused, so the agent does not lose a turn retrying.
|
|
7
|
+
|
|
3
8
|
## 0.6.17
|
|
4
9
|
|
|
5
10
|
### Patch Changes
|
package/dist/bin/serve.js
CHANGED
|
@@ -38,15 +38,9 @@
|
|
|
38
38
|
*/
|
|
39
39
|
import { existsSync, realpathSync } from 'node:fs';
|
|
40
40
|
import { fileURLToPath } from 'node:url';
|
|
41
|
-
import { createCheckMessagesTool, messageRelayInstructions } from '../server/message-relay.js';
|
|
42
41
|
import { isLoopbackHostname } from '@jini-ai/platform/net';
|
|
43
42
|
import { createMcpToolServer as createMcpToolServerDefault } from '../server/tool-server.js';
|
|
44
|
-
import {
|
|
45
|
-
import { describeToolTool, TOOL_CATALOG_TOOLS } from '../server/tools/tool-catalog-tools.js';
|
|
46
|
-
import { COMPONENT_CATALOG_TOOLS } from '../server/tools/component-catalog-tools.js';
|
|
47
|
-
import { createExecuteDelegatedToolTool, createExecuteReadonlyDelegatedToolTool } from '../server/tools/delegated-tool.js';
|
|
48
|
-
import { KERNEL_RESOURCES } from '../server/resources/active-resource.js';
|
|
49
|
-
import { createSiteGuideResource } from '../server/resources/site-guide-resource.js';
|
|
43
|
+
import { assembleRunToolHost } from '../server/run-tool-host.js';
|
|
50
44
|
/** The one run this process is scoped to for its entire lifetime. Set by the spawning daemon, never by the MCP client. */
|
|
51
45
|
export const RUN_ID_ENV_VAR = 'JINI_RUN_ID';
|
|
52
46
|
/** Checked (only) when resolving the daemon HTTP base URL — see `resolveDaemonUrl`'s own docs; matches the CLI entry point's identically-named constant. */
|
|
@@ -79,16 +73,6 @@ export const DELEGATED_TOOL_TIMEOUT_ENV_VAR = 'JINI_DELEGATED_TOOL_TIMEOUT_MS';
|
|
|
79
73
|
/** JSON array of host-selected visible registry IDs; never sourced from diagnostic IDs. */
|
|
80
74
|
export const STARTUP_TOOL_IDS_ENV_VAR = 'JINI_STARTUP_TOOL_IDS';
|
|
81
75
|
const SERVER_NAME = 'jini-mcp';
|
|
82
|
-
const SERVER_VERSION = '0.0.0';
|
|
83
|
-
const INSTRUCTIONS = 'Tools proxying this host\'s Jini daemon over loopback HTTP, scoped to the run that spawned this MCP server process. ' +
|
|
84
|
-
'Use execute_delegated_tool to invoke a Jini-registered tool (never an agent-vendor-specific tool name) against that run — ' +
|
|
85
|
-
'Registered operation calls use the daemon\'s deny-by-default ToolExecutor gate; ' +
|
|
86
|
-
'MCP-local names use their existing validated handlers. ' +
|
|
87
|
-
'Concrete operation tools accept their own arguments directly. describe_tool publishes a concrete definition; ' +
|
|
88
|
-
'refresh tools/list when supported, or use the delegated gateways as the compatibility fallback. ' +
|
|
89
|
-
'When the host provides site_guide, consult jini://site-guide or call site_guide with operation overview ' +
|
|
90
|
-
'through execute_readonly_delegated_tool; follow with explain for the relevant topic ids. ' +
|
|
91
|
-
'If the host does not provide site_guide, report that availability gap.';
|
|
92
76
|
// Keep the bin's environment-only assembly here; a general CLI resolver would introduce a
|
|
93
77
|
// runtime dependency on another shell. No discovery/default URL is selected for a run child.
|
|
94
78
|
async function resolveDaemonUrlDefault(_required, options) {
|
|
@@ -146,24 +130,20 @@ export async function serve(_requiredArgs, deps = {}) {
|
|
|
146
130
|
};
|
|
147
131
|
const delegatedGatewayOptions = buildDelegatedGatewayOptions({ env }, deps);
|
|
148
132
|
const messageRelay = resolveMessageRelay({ env, runId, writeErr, exit });
|
|
149
|
-
const requiredArgs = {
|
|
150
|
-
name: SERVER_NAME,
|
|
151
|
-
version: SERVER_VERSION,
|
|
152
|
-
tools: [...buildTools({ runId, delegatedGatewayOptions }), ...(messageRelay ? [createCheckMessagesTool(messageRelay)] : [])],
|
|
153
|
-
resolveBaseUrl,
|
|
154
|
-
};
|
|
155
|
-
const serverOptions = buildServerOptions({ env, runId, delegatedGatewayOptions }, deps);
|
|
156
|
-
if (messageRelay)
|
|
157
|
-
Object.assign(serverOptions, { messageRelay, instructions: `${INSTRUCTIONS} ${messageRelayInstructions(messageRelay)}` });
|
|
158
133
|
try {
|
|
159
134
|
const diagnosticToolIds = parseLaunchToolIds({ env, key: 'JINI_DIAGNOSTIC_TOOL_IDS' });
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
135
|
+
// Absent credential => no `authHeaders` at all => byte-identical request headers to before this
|
|
136
|
+
// env var existed. See DAEMON_TOKEN_ENV_VAR's doc for why an unset value is not a startup failure.
|
|
137
|
+
const assembled = await assembleRunToolHost({ runId, resolveBaseUrl }, {
|
|
138
|
+
...delegatedGatewayOptions,
|
|
139
|
+
...definedOnly({ credential: env[DAEMON_TOKEN_ENV_VAR], fetchImpl: deps.fetchImpl,
|
|
140
|
+
messageRelayNonce: messageRelay?.nonce }),
|
|
141
|
+
startupToolIds: parseLaunchToolIds({ env, key: STARTUP_TOOL_IDS_ENV_VAR }),
|
|
142
|
+
...(diagnosticToolIds.length ? { diagnosticToolIds } : {}),
|
|
164
143
|
});
|
|
165
|
-
|
|
166
|
-
|
|
144
|
+
await createMcpToolServerFn(assembled.required, { ...assembled.options,
|
|
145
|
+
...definedOnly({ stdin: deps.stdin, stdout: deps.stdout }),
|
|
146
|
+
}).run({});
|
|
167
147
|
}
|
|
168
148
|
catch (err) {
|
|
169
149
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -211,33 +191,6 @@ function buildDelegatedGatewayOptions({ env }, deps) {
|
|
|
211
191
|
delegatedToolTimeoutMs: rawTimeout === undefined ? undefined : Number(rawTimeout),
|
|
212
192
|
});
|
|
213
193
|
}
|
|
214
|
-
function buildTools({ runId, delegatedGatewayOptions }) {
|
|
215
|
-
return [
|
|
216
|
-
...RUN_TOOLS,
|
|
217
|
-
...TOOL_CATALOG_TOOLS,
|
|
218
|
-
...COMPONENT_CATALOG_TOOLS,
|
|
219
|
-
createExecuteDelegatedToolTool({ runId }, delegatedGatewayOptions),
|
|
220
|
-
// Hosted ALONGSIDE the unconstrained gateway, never instead of it. A client that gates on
|
|
221
|
-
// `readOnlyHint: true` can now reach the read half of the catalog at all; every other client
|
|
222
|
-
// sees both and picks by what its call actually does.
|
|
223
|
-
createExecuteReadonlyDelegatedToolTool({ runId }, delegatedGatewayOptions),
|
|
224
|
-
];
|
|
225
|
-
}
|
|
226
|
-
function buildServerOptions({ env, runId, delegatedGatewayOptions }, deps) {
|
|
227
|
-
// Absent credential => no `authHeaders` at all => byte-identical request headers to before this
|
|
228
|
-
// env var existed. See DAEMON_TOKEN_ENV_VAR's doc for why an unset value is not a startup failure.
|
|
229
|
-
const credential = env[DAEMON_TOKEN_ENV_VAR];
|
|
230
|
-
return {
|
|
231
|
-
resources: [...KERNEL_RESOURCES, createSiteGuideResource({ runId }, delegatedGatewayOptions)],
|
|
232
|
-
instructions: INSTRUCTIONS,
|
|
233
|
-
...definedOnly({
|
|
234
|
-
authHeaders: credential ? { Authorization: `Bearer ${credential}` } : undefined,
|
|
235
|
-
fetchImpl: deps.fetchImpl,
|
|
236
|
-
stdin: deps.stdin,
|
|
237
|
-
stdout: deps.stdout,
|
|
238
|
-
}),
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
194
|
// Only the spawning host supplies registration metadata. Never learn exceptions from an
|
|
242
195
|
// error body or model arguments: either would turn a secret into its own redaction exemption.
|
|
243
196
|
function parseLaunchToolIds({ env, key }) {
|
|
@@ -247,28 +200,6 @@ function parseLaunchToolIds({ env, key }) {
|
|
|
247
200
|
}
|
|
248
201
|
return toolIds;
|
|
249
202
|
}
|
|
250
|
-
/**
|
|
251
|
-
* Resolve each unique startup ID through the existing authenticated describe/publication owner.
|
|
252
|
-
* The temporary descriptor list seeds M1's bounded exposure; it is not an execution catalog.
|
|
253
|
-
* Failure aborts startup before any partially resolved list can reach the client.
|
|
254
|
-
* @returns Authoritative descriptors; no daemon I/O when startup IDs are absent.
|
|
255
|
-
* @throws The existing descriptor/daemon errors, reported by serve's stderr/exit boundary.
|
|
256
|
-
* @complexity O(n + s) space/time plus n sequential bounded GETs for n unique IDs and s schema bytes.
|
|
257
|
-
*/
|
|
258
|
-
async function resolveStartupTools({ toolIds, resolveBaseUrl, serverOptions }) {
|
|
259
|
-
if (toolIds.length === 0)
|
|
260
|
-
return [];
|
|
261
|
-
const descriptors = [];
|
|
262
|
-
const ctx = {
|
|
263
|
-
baseUrl: await resolveBaseUrl(),
|
|
264
|
-
fetchImpl: serverOptions.fetchImpl ?? fetch,
|
|
265
|
-
...definedOnly({ authHeaders: serverOptions.authHeaders, diagnosticToolIds: serverOptions.diagnosticToolIds }),
|
|
266
|
-
publishTool: async ({ descriptor }) => { descriptors.push(descriptor); },
|
|
267
|
-
};
|
|
268
|
-
for (const id of new Set(toolIds))
|
|
269
|
-
await describeToolTool.handler({ args: { id }, ctx });
|
|
270
|
-
return descriptors;
|
|
271
|
-
}
|
|
272
203
|
// Only run for real when this module is the actual process entrypoint (`node dist/bin/serve.js`,
|
|
273
204
|
// or the `jini-mcp` bin shim pnpm/npm links to it) — not merely imported, e.g. by this file's own
|
|
274
205
|
// tests importing the named `serve` export above. Mirrors the CLI entry point's identical guard.
|
package/dist/core/config.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
1
3
|
import { isLoopbackHostname } from '@jini-ai/platform/net';
|
|
2
4
|
/**
|
|
3
5
|
* @module @jini-ai/mcp/core/config
|
|
@@ -232,7 +234,7 @@ export async function readMcpConfig({ dataDir }, { filesystem = defaultFilesyste
|
|
|
232
234
|
if (e.code === 'ENOENT')
|
|
233
235
|
return { servers: [] };
|
|
234
236
|
if (e.name === 'SyntaxError') {
|
|
235
|
-
console.error('[mcp-config] Corrupted JSON, returning empty:', e
|
|
237
|
+
console.error('[mcp-config] Corrupted JSON, returning empty:', redactSecrets({ input: inspect(e) }, { redactPii: false }));
|
|
236
238
|
return { servers: [] };
|
|
237
239
|
}
|
|
238
240
|
throw err;
|
package/dist/core/tokens.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
1
3
|
import { createSystemClock } from "@jini-ai/core/primitives";
|
|
2
4
|
/**
|
|
3
5
|
* @module @jini-ai/mcp/core/tokens
|
|
@@ -145,7 +147,7 @@ export async function readTokensFile({ dataDir }, options = {}) {
|
|
|
145
147
|
filePath: `${tokensFile(dataDir)}.corrupt-${digest}`,
|
|
146
148
|
contents: raw,
|
|
147
149
|
});
|
|
148
|
-
console.error('[mcp-tokens] Corrupted JSON, returning empty:', e
|
|
150
|
+
console.error('[mcp-tokens] Corrupted JSON, returning empty:', redactSecrets({ input: inspect(e) }, { redactPii: false }));
|
|
149
151
|
return { ...EMPTY, servers: { ...EMPTY.servers } };
|
|
150
152
|
}
|
|
151
153
|
throw err;
|
|
@@ -46,10 +46,9 @@ export interface AttachFederatedToolsResult {
|
|
|
46
46
|
* connection was simply invisible everywhere an operator could look — `GET
|
|
47
47
|
* /api/federation/admissions` showed no row for it and `configFailures` (a SEPARATE, boot-time
|
|
48
48
|
* config-resolution channel — see `external-mcp-connection-source.ts`) has no way to know about a
|
|
49
|
-
* failure that happens deeper, inside `connect()` itself.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* since nothing in this file's `connect`/`spawn` error paths ever formats one into a message.
|
|
49
|
+
* failure that happens deeper, inside `connect()` itself. The message is redacted before it
|
|
50
|
+
* reaches this caller-visible field: remote failures can echo tokens and credentialed URLs,
|
|
51
|
+
* and the log's separate redaction does not protect the returned value.
|
|
53
52
|
*/
|
|
54
53
|
readonly connectFailures: readonly {
|
|
55
54
|
readonly connectionId: string;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
1
3
|
import { createConsoleLogger } from "@jini-ai/core/primitives";
|
|
2
4
|
import { defaultFederatedMcpPresetRegistry } from "./presets.js";
|
|
3
5
|
import { federateSession } from "./registrations.js";
|
|
@@ -142,13 +144,19 @@ async function attachOneFederatedConnection(params) {
|
|
|
142
144
|
const registeredToolIds = registerFederatedTools(registry, registrations);
|
|
143
145
|
logger.info({ message: `mcp-federation: '${connectionId}' registered ${registrations.length} federated tool(s): ${registrations.map((r) => r.descriptor.id).join(", ") || "(none)"}` });
|
|
144
146
|
logFederatedAdmissionReport(connectionId, report, logger);
|
|
147
|
+
// An empty grant has no callable handler retaining this session. Release its child/socket
|
|
148
|
+
// before a later operator grant re-admits the row with a new session.
|
|
149
|
+
if (connection.config.allowedToolNames.length === 0) {
|
|
150
|
+
await session.close({});
|
|
151
|
+
return { registeredToolIds, session: null, report };
|
|
152
|
+
}
|
|
145
153
|
return { registeredToolIds, session, report };
|
|
146
154
|
}
|
|
147
155
|
catch (error) {
|
|
148
|
-
logger.warn({ message: `mcp-federation: '${connectionId}' failed, continuing without its tools — ${
|
|
156
|
+
logger.warn({ message: `mcp-federation: '${connectionId}' failed, continuing without its tools — ${redactSecrets({ input: inspect(error) }, { redactPii: false })}` });
|
|
149
157
|
// A session that connected but failed during listing/admission still owns a child process.
|
|
150
158
|
await session?.close({}).catch(() => undefined);
|
|
151
|
-
return { registeredToolIds: [], session: null, report: null, connectFailureReason: messageOf(error) };
|
|
159
|
+
return { registeredToolIds: [], session: null, report: null, connectFailureReason: redactSecrets({ input: messageOf(error) }, { redactPii: false }) };
|
|
152
160
|
}
|
|
153
161
|
}
|
|
154
162
|
/**
|
|
@@ -171,7 +179,7 @@ export function resolveFederatedMcpPresets({ env, logger, registry }, _optional
|
|
|
171
179
|
connections.push(connection);
|
|
172
180
|
}
|
|
173
181
|
catch (error) {
|
|
174
|
-
logger.warn({ message: `mcp-federation: preset '${preset.presetId}' configuration is invalid, continuing without federated tools — ${
|
|
182
|
+
logger.warn({ message: `mcp-federation: preset '${preset.presetId}' configuration is invalid, continuing without federated tools — ${redactSecrets({ input: inspect(error) }, { redactPii: false })}` });
|
|
175
183
|
}
|
|
176
184
|
}
|
|
177
185
|
return connections;
|
|
@@ -14,7 +14,7 @@ import type { FederationDeps } from "./registrations.js";
|
|
|
14
14
|
*
|
|
15
15
|
* `trust.ts` R5 freezes an ALREADY-ADMITTED connection's tool set at connect, forever, for this
|
|
16
16
|
* process's lifetime — the rug-pull protection that file's own header explains. This module never
|
|
17
|
-
* re-lists, re-classifies, or re-admits a connectionId that has
|
|
17
|
+
* re-lists, re-classifies, or re-admits a connectionId with admitted tools that has gone through
|
|
18
18
|
* {@link attachFederatedMcpTools} once, whether that pass ran at boot or at an earlier reload
|
|
19
19
|
* ({@link selectUnadmittedConnections} is the whole enforcement mechanism: a connectionId already in
|
|
20
20
|
* `admittedConnectionIds` is filtered out before `attach` is ever called on it again). It only ever
|
|
@@ -22,6 +22,9 @@ import type { FederationDeps } from "./registrations.js";
|
|
|
22
22
|
* is the same guarantee a fresh boot already gives, just applied to a connection that did not exist
|
|
23
23
|
* yet when this process started. `ToolRegistry` (`@jini-ai/core`) is deliberately append-only with no
|
|
24
24
|
* unregister, so this module still has no way to revise or drop an admitted tool even if it wanted to.
|
|
25
|
+
* A roster row admitted with an empty allowlist and no tools can wait for its first operator grant.
|
|
26
|
+
* Retrying that row replaces no tool or classification; after its first nonempty admission it freezes
|
|
27
|
+
* just like every other connection. An unchanged empty grant never triggers another handshake.
|
|
25
28
|
*
|
|
26
29
|
* That admission freeze no longer means an operator's edit is inert until restart, though —
|
|
27
30
|
* `external-mcp-revocation.ts`'s per-call gate (`mcp-federation/registrations.ts`'s
|
|
@@ -108,5 +111,7 @@ export interface FederationReloadCoordinator {
|
|
|
108
111
|
export declare function createFederationReloadCoordinator({ coordDeps, initiallyAdmitted }: {
|
|
109
112
|
coordDeps: FederationReloadCoordinatorDeps;
|
|
110
113
|
initiallyAdmitted: Iterable<string>;
|
|
114
|
+
}, { initiallyAwaitingGrants }?: {
|
|
115
|
+
initiallyAwaitingGrants?: Iterable<string>;
|
|
111
116
|
}): FederationReloadCoordinator;
|
|
112
117
|
//# sourceMappingURL=reload.d.ts.map
|
|
@@ -11,7 +11,7 @@ import { attachFederatedMcpTools } from "./bootstrap.js";
|
|
|
11
11
|
*
|
|
12
12
|
* `trust.ts` R5 freezes an ALREADY-ADMITTED connection's tool set at connect, forever, for this
|
|
13
13
|
* process's lifetime — the rug-pull protection that file's own header explains. This module never
|
|
14
|
-
* re-lists, re-classifies, or re-admits a connectionId that has
|
|
14
|
+
* re-lists, re-classifies, or re-admits a connectionId with admitted tools that has gone through
|
|
15
15
|
* {@link attachFederatedMcpTools} once, whether that pass ran at boot or at an earlier reload
|
|
16
16
|
* ({@link selectUnadmittedConnections} is the whole enforcement mechanism: a connectionId already in
|
|
17
17
|
* `admittedConnectionIds` is filtered out before `attach` is ever called on it again). It only ever
|
|
@@ -19,6 +19,9 @@ import { attachFederatedMcpTools } from "./bootstrap.js";
|
|
|
19
19
|
* is the same guarantee a fresh boot already gives, just applied to a connection that did not exist
|
|
20
20
|
* yet when this process started. `ToolRegistry` (`@jini-ai/core`) is deliberately append-only with no
|
|
21
21
|
* unregister, so this module still has no way to revise or drop an admitted tool even if it wanted to.
|
|
22
|
+
* A roster row admitted with an empty allowlist and no tools can wait for its first operator grant.
|
|
23
|
+
* Retrying that row replaces no tool or classification; after its first nonempty admission it freezes
|
|
24
|
+
* just like every other connection. An unchanged empty grant never triggers another handshake.
|
|
22
25
|
*
|
|
23
26
|
* That admission freeze no longer means an operator's edit is inert until restart, though —
|
|
24
27
|
* `external-mcp-revocation.ts`'s per-call gate (`mcp-federation/registrations.ts`'s
|
|
@@ -57,28 +60,44 @@ export function selectUnadmittedConnections({ allConnections, admittedConnection
|
|
|
57
60
|
* @complexity `reload()` is O(c · t) in the unadmitted connection count and their advertised tools
|
|
58
61
|
* (the underlying `attachFederatedMcpTools` cost); `admittedConnectionIds()` is O(1).
|
|
59
62
|
*/
|
|
60
|
-
export function createFederationReloadCoordinator({ coordDeps, initiallyAdmitted }) {
|
|
63
|
+
export function createFederationReloadCoordinator({ coordDeps, initiallyAdmitted }, { initiallyAwaitingGrants = [] } = {}) {
|
|
61
64
|
const admitted = new Set(initiallyAdmitted);
|
|
65
|
+
const awaitingGrants = new Set(initiallyAwaitingGrants);
|
|
62
66
|
const attach = coordDeps.attach ?? attachFederatedMcpTools;
|
|
63
67
|
let inFlight = null;
|
|
64
68
|
let queuedNext = null;
|
|
65
69
|
async function runOnePass() {
|
|
66
70
|
const allConnections = await coordDeps.resolveConnections();
|
|
67
|
-
const
|
|
71
|
+
const eligibleIds = new Set(admitted);
|
|
72
|
+
for (const connection of allConnections) {
|
|
73
|
+
if (awaitingGrants.has(connection.config.connectionId) && connection.config.allowedToolNames.length > 0)
|
|
74
|
+
eligibleIds.delete(connection.config.connectionId);
|
|
75
|
+
}
|
|
76
|
+
const unadmitted = selectUnadmittedConnections({ allConnections, admittedConnectionIds: eligibleIds });
|
|
68
77
|
if (unadmitted.length === 0)
|
|
69
78
|
return { newlyAdmittedConnectionIds: [], reports: [], connectFailures: [] };
|
|
70
79
|
// Empty, not omitted: omission resolves presets from the environment again, even though the
|
|
71
80
|
// boot pass already admitted them. Reload concerns only the operator-editable roster;
|
|
72
81
|
// environment presets cannot change without a real restart.
|
|
73
82
|
const attached = await attach({ registry: coordDeps.registry, deps: coordDeps.deps, connect: coordDeps.connect }, { connections: [], extraConnections: unadmitted, ...(coordDeps.logger ? { logger: coordDeps.logger } : {}) });
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
const attemptedConnections = new Map(unadmitted.map(connection => [connection.config.connectionId, connection]));
|
|
84
|
+
for (const entry of attached.reports) {
|
|
85
|
+
rememberAdmission({ entry, connection: attemptedConnections.get(entry.connectionId) });
|
|
86
|
+
}
|
|
76
87
|
return {
|
|
77
88
|
newlyAdmittedConnectionIds: attached.reports.map((entry) => entry.connectionId),
|
|
78
89
|
reports: attached.reports,
|
|
79
90
|
connectFailures: attached.connectFailures,
|
|
80
91
|
};
|
|
81
92
|
}
|
|
93
|
+
/** Only a successful empty-grant admission remains eligible for its first operator grant. O(1). */
|
|
94
|
+
function rememberAdmission({ entry, connection }, _options = {}) {
|
|
95
|
+
admitted.add(entry.connectionId);
|
|
96
|
+
if (connection?.config.allowedToolNames.length === 0 && entry.report.admitted.length === 0)
|
|
97
|
+
awaitingGrants.add(entry.connectionId);
|
|
98
|
+
else
|
|
99
|
+
awaitingGrants.delete(entry.connectionId);
|
|
100
|
+
}
|
|
82
101
|
function reload(_required) {
|
|
83
102
|
if (!inFlight) {
|
|
84
103
|
inFlight = runOnePass().finally(() => {
|
package/dist/index.d.ts
CHANGED
|
@@ -12,8 +12,8 @@ export type { StoredMcpToken, McpTokensFile } from './core/index.js';
|
|
|
12
12
|
export { buildMcpInstallPayload } from './core/index.js';
|
|
13
13
|
export type { BuildMcpInstallPayloadInputs, McpInstallPayload } from './core/index.js';
|
|
14
14
|
export { createMcpIdleExitController, extractRelativeRefs, isTextualMime, } from './client/index.js';
|
|
15
|
-
export { cancelRunTool, createMcpToolServer, daemonCallOptions, errorResult, getActiveContextTool, getDaemonJson, getRunTool, listAgentsTool, okResult, postDaemonJson, requireString, RUN_TOOLS, startRunTool, toolsToList, buildToolIndex, handleToolCall, DaemonResponseTooLargeError, } from './server/index.js';
|
|
16
|
-
export type { DaemonRequestOptions, McpServerLike, McpToolContext, McpToolDef, McpToolServerHandle, McpToolServerOptions, McpTransportLike, } from './server/index.js';
|
|
15
|
+
export { cancelRunTool, createMcpToolServer, startMcpHttpToolServer, createMcpHttpToolServerRuntime, daemonCallOptions, errorResult, getActiveContextTool, getDaemonJson, getRunTool, listAgentsTool, okResult, postDaemonJson, requireString, RUN_TOOLS, startRunTool, toolsToList, buildToolIndex, handleToolCall, DaemonResponseTooLargeError, } from './server/index.js';
|
|
16
|
+
export type { DaemonRequestOptions, McpServerLike, McpToolContext, McpToolDef, McpToolServerHandle, McpHttpToolServerHandle, McpHttpToolServerRuntime, McpHttpToolServerOptions, McpToolServerOptions, McpTransportLike, } from './server/index.js';
|
|
17
17
|
export { activeContextResource, createSiteGuideResource, buildResourceIndex, handleResourceRead, KERNEL_RESOURCES, resourcesToList, } from './server/index.js';
|
|
18
18
|
export type { McpResourceDef, McpResourceReadResult } from './server/index.js';
|
|
19
19
|
export { searchToolsTool, describeToolTool, TOOL_CATALOG_TOOLS } from './server/index.js';
|
|
@@ -29,4 +29,7 @@ export type { McpIdleTimerPort } from './client/client.js';
|
|
|
29
29
|
export type { McpToolServerRequiredArgs } from './server/tool-server.js';
|
|
30
30
|
export type { CreateExecuteDelegatedToolToolRequiredArgs } from './server/tools/delegated-tool.js';
|
|
31
31
|
export { DaemonHttpError } from './server/daemon-client.js';
|
|
32
|
+
export { assembleRunToolHost } from './server/run-tool-host.js';
|
|
33
|
+
export type { RunToolHostInput, RunToolHostOptions, RunToolHost } from './server/run-tool-host.js';
|
|
34
|
+
export { MCP_HTTP_SESSION_MODE_SUPPORTED } from './server/http-tool-server.js';
|
|
32
35
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { buildMcpInstallPayload } from './core/index.js';
|
|
|
14
14
|
// ── client: product-neutral stdio-server runtime primitives ─────────────────
|
|
15
15
|
export { createMcpIdleExitController, extractRelativeRefs, isTextualMime, } from './client/index.js';
|
|
16
16
|
// ── server: the generic MCP tool-hosting mechanism + kernel-run tool defs ───
|
|
17
|
-
export { cancelRunTool, createMcpToolServer, daemonCallOptions, errorResult, getActiveContextTool, getDaemonJson, getRunTool, listAgentsTool, okResult, postDaemonJson, requireString, RUN_TOOLS, startRunTool, toolsToList, buildToolIndex, handleToolCall, DaemonResponseTooLargeError, } from './server/index.js';
|
|
17
|
+
export { cancelRunTool, createMcpToolServer, startMcpHttpToolServer, createMcpHttpToolServerRuntime, daemonCallOptions, errorResult, getActiveContextTool, getDaemonJson, getRunTool, listAgentsTool, okResult, postDaemonJson, requireString, RUN_TOOLS, startRunTool, toolsToList, buildToolIndex, handleToolCall, DaemonResponseTooLargeError, } from './server/index.js';
|
|
18
18
|
// ── server: the generic MCP resource surface + kernel resource defs ────────
|
|
19
19
|
export { activeContextResource, createSiteGuideResource, buildResourceIndex, handleResourceRead, KERNEL_RESOURCES, resourcesToList, } from './server/index.js';
|
|
20
20
|
// ── server: the tool-catalog discovery defs (search_tools / describe_tool) ──
|
|
@@ -26,4 +26,6 @@ export { createExecuteDelegatedToolTool, createExecuteReadonlyDelegatedToolTool,
|
|
|
26
26
|
// ── agent-install: register an MCP server into external agents ──────────────
|
|
27
27
|
export { CHROME_DEVTOOLS_MCP_PRESET, AGENT_SLUGS, isAgentSlug, planAgentInstall, verifyJsonAgentInstall, applyJsonInstall, removeJsonInstall, } from './agent-install/index.js';
|
|
28
28
|
export { DaemonHttpError } from './server/daemon-client.js';
|
|
29
|
+
export { assembleRunToolHost } from './server/run-tool-host.js';
|
|
30
|
+
export { MCP_HTTP_SESSION_MODE_SUPPORTED } from './server/http-tool-server.js';
|
|
29
31
|
//# sourceMappingURL=index.js.map
|
|
@@ -22,8 +22,8 @@ export interface DaemonRequestOptions {
|
|
|
22
22
|
/** Defaults to the global `fetch`; inject for tests. */
|
|
23
23
|
fetchImpl?: typeof fetch;
|
|
24
24
|
headers?: Record<string, string>;
|
|
25
|
-
/** Request deadline in ms.
|
|
26
|
-
timeoutMs?: number;
|
|
25
|
+
/** Request deadline in ms. Null delegates lifetime to caller/run cancellation; omission uses DEFAULT_TIMEOUT_MS. */
|
|
26
|
+
timeoutMs?: number | null;
|
|
27
27
|
/** Maximum daemon response size in bytes before the read is aborted. Defaults to {@link DEFAULT_MAX_RESPONSE_BYTES}. */
|
|
28
28
|
maxResponseBytes?: number;
|
|
29
29
|
/** Caller-supplied cancellation signal; combined with the internal timeout signal, so either one aborts the request. */
|
|
@@ -155,6 +155,9 @@ function formatDaemonHttpError({ status, data }, options) {
|
|
|
155
155
|
*/
|
|
156
156
|
function startDeadline({ route, timeoutMs }, { signal }) {
|
|
157
157
|
const timeoutController = new AbortController();
|
|
158
|
+
// An interactive delegated call is bounded by its run/caller cancellation, not human reading time.
|
|
159
|
+
if (timeoutMs === null)
|
|
160
|
+
return { signal: signal ?? timeoutController.signal, clear: () => { } };
|
|
158
161
|
const timeoutHandle = setTimeout(() => {
|
|
159
162
|
timeoutController.abort(new Error(`request to ${route} timed out after ${timeoutMs}ms`));
|
|
160
163
|
}, timeoutMs);
|
|
@@ -192,7 +195,7 @@ async function readBodyOrExplain({ resp, url, maxResponseBytes }, options) {
|
|
|
192
195
|
async function requestDaemonJson(baseUrl, route, init, options = {}) {
|
|
193
196
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
194
197
|
const url = `${baseUrl}${route}`;
|
|
195
|
-
const deadline = startDeadline({ route, timeoutMs: options.timeoutMs
|
|
198
|
+
const deadline = startDeadline({ route, timeoutMs: options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : options.timeoutMs }, { signal: options.signal });
|
|
196
199
|
try {
|
|
197
200
|
let resp;
|
|
198
201
|
try {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse, type Server as HttpServer } from 'node:http';
|
|
2
|
+
import { type McpToolServerRequiredArgs, type McpToolServerOptions } from './tool-server.js';
|
|
3
|
+
export interface McpHttpToolServerOptions extends Pick<McpToolServerOptions, 'resources' | 'instructions' | 'fetchImpl' | 'authHeaders' | 'delegatedTools' | 'resultPaging' | 'diagnosticToolIds' | 'messageRelay'> {
|
|
4
|
+
/** One SDK session per run listener; desktop remains stateless by default. */
|
|
5
|
+
readonly sessionMode?: boolean;
|
|
6
|
+
/** Value-free diagnostics. Never receives headers, bodies, tool arguments or credentials. */
|
|
7
|
+
readonly warn?: (message: string) => void;
|
|
8
|
+
/** Native listener seam for embeddings and wire-level lifecycle tests. */
|
|
9
|
+
readonly createHttpServer?: (listener: (request: IncomingMessage, response: ServerResponse) => void) => HttpServer;
|
|
10
|
+
}
|
|
11
|
+
export interface McpHttpToolServerHandle {
|
|
12
|
+
readonly url: string;
|
|
13
|
+
readonly token: string;
|
|
14
|
+
/** Stop admission and drain accepted work; no idle timer interrupts handlers. */
|
|
15
|
+
close(required: Record<string, never>, optional?: Record<string, never>): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/** Embeddings must gate CLI HTTP on retained-session support rather than a protocol probe. */
|
|
18
|
+
export declare const MCP_HTTP_SESSION_MODE_SUPPORTED = true;
|
|
19
|
+
/** Bind a private loopback listener. Desktop requests use fresh stateless SDK transports;
|
|
20
|
+
* run listeners opt into one retained SDK session and the same host-owned dispatch contract. Auth precedes body parsing; browsers are always refused.
|
|
21
|
+
* @example const host = await startMcpHttpToolServer({ name, version, tools, resolveBaseUrl });
|
|
22
|
+
* @throws Listen/configuration errors; the embedding owns degraded startup and retries.
|
|
23
|
+
* @complexity O(body bytes + dispatched handler), with 16 requests and 1 MB per body maximum.
|
|
24
|
+
*/
|
|
25
|
+
export declare function startMcpHttpToolServer(required: McpToolServerRequiredArgs, options?: McpHttpToolServerOptions): Promise<McpHttpToolServerHandle>;
|
|
26
|
+
export interface McpHttpToolServerRuntime {
|
|
27
|
+
/** Best-effort startup: absent until a listener binds; failures repair in the background. */
|
|
28
|
+
capability(required: Record<string, never>, optional?: Record<string, never>): Promise<Pick<McpHttpToolServerHandle, 'url' | 'token'> | undefined>;
|
|
29
|
+
/** Deliver a capability after binding, including initial recovery. Failed handoffs retry;
|
|
30
|
+
* unsubscribe cancels pending deliveries. This must never be used to replay tool calls. */
|
|
31
|
+
subscribe(required: {
|
|
32
|
+
listener: (capability: Pick<McpHttpToolServerHandle, 'url' | 'token'>) => void | Promise<void>;
|
|
33
|
+
}, optional?: Record<string, never>): () => void;
|
|
34
|
+
close(required: Record<string, never>, optional?: Record<string, never>): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** Own one shared HTTP listener independently of site/window lifecycles. No I/O until capability()
|
|
37
|
+
* is called. Concurrent callers share a start; shutdown cancels retries and drains a pending bind.
|
|
38
|
+
* @example const runtime = createMcpHttpToolServerRuntime({ name, version, tools, resolveBaseUrl });
|
|
39
|
+
* @complexity O(1) state beyond the bounded HTTP listener and caller-supplied tool table.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createMcpHttpToolServerRuntime(required: McpToolServerRequiredArgs, { start, ...options }?: McpHttpToolServerOptions & {
|
|
42
|
+
start?: typeof startMcpHttpToolServer;
|
|
43
|
+
}): McpHttpToolServerRuntime;
|
|
44
|
+
//# sourceMappingURL=http-tool-server.d.ts.map
|