@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 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 { RUN_TOOLS } from '../server/tools/run-tools.js';
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
- if (diagnosticToolIds.length)
161
- Object.assign(serverOptions, { diagnosticToolIds });
162
- const initialTools = await resolveStartupTools({
163
- toolIds: parseLaunchToolIds({ env, key: STARTUP_TOOL_IDS_ENV_VAR }), resolveBaseUrl, serverOptions,
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
- Object.assign(serverOptions, { delegatedTools: { runId, ...delegatedGatewayOptions, initialTools } });
166
- await createMcpToolServerFn(requiredArgs, serverOptions).run({});
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.
@@ -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.message);
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;
@@ -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.message);
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. `reason` is `messageOf(error)` — the
50
- * same text `logger.warn` already prints for this exact failure, so this field discloses nothing
51
- * that was not already reaching this process's own stderr; it is never a raw env value or secret,
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 — ${messageOf(error)}` });
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 — ${messageOf(error)}` });
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 already gone through
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 already gone through
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 unadmitted = selectUnadmittedConnections({ allConnections: allConnections, admittedConnectionIds: admitted });
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
- for (const entry of attached.reports)
75
- admitted.add(entry.connectionId);
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. Defaults to {@link DEFAULT_TIMEOUT_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 ?? DEFAULT_TIMEOUT_MS }, { signal: options.signal });
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