@jini-ai/mcp 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -0
- package/dist/agent-install/chrome-devtools.d.ts +4 -0
- package/dist/agent-install/chrome-devtools.js +7 -0
- package/dist/agent-install/index.d.ts +1 -0
- package/dist/agent-install/index.js +1 -0
- package/dist/agent-install/install.js +102 -194
- package/dist/bin/serve.d.ts +4 -1
- package/dist/bin/serve.js +120 -45
- package/dist/client/client.js +71 -58
- package/dist/core/config.js +113 -149
- package/dist/core/tokens.js +29 -53
- package/dist/federation/adapter.http.js +3 -2
- package/dist/federation/approvals/call-confirmation.js +5 -70
- package/dist/federation/approvals/remembered-approval.d.ts +39 -0
- package/dist/federation/approvals/remembered-approval.js +98 -0
- package/dist/federation/approvals/revocation-reason.d.ts +9 -0
- package/dist/federation/approvals/revocation-reason.js +11 -0
- package/dist/federation/approvals/revocation.js +19 -13
- package/dist/federation/approvals/scope-option.d.ts +6 -0
- package/dist/federation/approvals/scope-option.js +6 -0
- package/dist/federation/bootstrap.d.ts +18 -0
- package/dist/federation/bootstrap.js +4 -4
- package/dist/federation/presets.d.ts +16 -17
- package/dist/federation/presets.js +35 -28
- package/dist/federation/stdio/adapter.stdio.js +14 -6
- package/dist/federation/stdio/default-connect.js +24 -27
- package/dist/federation/stdio/stdio-launch-resolver.d.ts +1 -1
- package/dist/federation/stdio/stdio-launch-resolver.js +13 -9
- package/dist/federation/tool-approvals.js +3 -1
- package/dist/federation/trust.d.ts +23 -2
- package/dist/federation/trust.js +69 -35
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/server/daemon-client.d.ts +2 -0
- package/dist/server/daemon-client.js +86 -69
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/resources/site-guide-resource.d.ts +15 -0
- package/dist/server/resources/site-guide-resource.js +25 -0
- package/dist/server/tool-arguments.d.ts +46 -0
- package/dist/server/tool-arguments.js +111 -0
- package/dist/server/tool-protocol.d.ts +62 -7
- package/dist/server/tool-protocol.js +30 -40
- package/dist/server/tool-server.d.ts +19 -0
- package/dist/server/tool-server.js +91 -19
- package/dist/server/tools/delegated-tool.d.ts +47 -0
- package/dist/server/tools/delegated-tool.js +35 -2
- package/dist/server/tools/tool-catalog-tools.js +30 -8
- package/dist/tools/ask-choice/index.d.ts +213 -14
- package/dist/tools/ask-choice/index.js +90 -46
- package/dist/tools/ask-choice/question.d.ts +5 -1
- package/dist/tools/ask-choice/question.js +36 -2
- package/dist/tools/ask-choice/schema.d.ts +196 -10
- package/dist/tools/ask-choice/schema.js +33 -12
- package/dist/tools/ask-choice/types.d.ts +8 -0
- package/package.json +6 -5
- package/dist/core/oauth.d.ts +0 -274
- package/dist/core/oauth.js +0 -667
- package/dist/core/secure-write.d.ts +0 -17
- package/dist/core/secure-write.js +0 -72
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @jini-ai/mcp/server/tool-arguments
|
|
3
|
+
*
|
|
4
|
+
* The argument-shape half of `tools/call` dispatch, split out of `./tool-protocol.js`: copying
|
|
5
|
+
* caller arguments into a tool's canonical wrapper shape before schema validation, and turning
|
|
6
|
+
* validator errors into one field-specific recovery instruction. Pure functions, no I/O. Not
|
|
7
|
+
* re-exported by the `./index.js` barrel — {@link handleToolCall} in `./tool-protocol.js` is the
|
|
8
|
+
* only production caller.
|
|
9
|
+
* See docs/decisions/DR-005-proxy-argument-shapes.md.
|
|
10
|
+
*/
|
|
11
|
+
import type { OutputUnit } from '@cfworker/json-schema';
|
|
12
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
+
/** Argument normalization's input contract, shared with the dispatcher without a reverse import. */
|
|
14
|
+
export interface McpToolArgumentContract {
|
|
15
|
+
readonly inputSchema: Tool['inputSchema'];
|
|
16
|
+
/** Compatibility rules applied once before strict schema validation; never sent on the wire. */
|
|
17
|
+
readonly argumentNormalization?: {
|
|
18
|
+
readonly aliases?: Readonly<Record<string, string>>;
|
|
19
|
+
readonly foldIntoInput?: true;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** True for a plain JSON object (not `null`, not an array). */
|
|
23
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* Copies caller arguments into their canonical wrapper shape before schema validation.
|
|
26
|
+
* @param requiredArgs - Tool contract and untrusted arguments; neither is mutated.
|
|
27
|
+
* @param _options - Reserved normalization options.
|
|
28
|
+
* @returns Canonical arguments; nested tool input is copied only when fields are folded.
|
|
29
|
+
* @throws Error naming an ambiguous alias, duplicate input key, or invalid wrapper field.
|
|
30
|
+
* @complexity O(k + a + i) time and space for k argument keys, a aliases, and i input keys.
|
|
31
|
+
*/
|
|
32
|
+
export declare function normalizeToolArguments({ tool, rawArgs }: {
|
|
33
|
+
tool: McpToolArgumentContract;
|
|
34
|
+
rawArgs: unknown;
|
|
35
|
+
}, _options?: Record<string, never>): Record<string, unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* Formats structured validation errors as field-specific recovery instructions.
|
|
38
|
+
* @param requiredArgs - Validator errors, including JSON Pointer locations and keywords.
|
|
39
|
+
* @param _options - Reserved formatting options.
|
|
40
|
+
* @returns One actionable diagnostic; no schema-location trace.
|
|
41
|
+
* @complexity O(e + p) for e errors and p characters in the selected field path.
|
|
42
|
+
*/
|
|
43
|
+
export declare function argumentShapeMessage({ errors }: {
|
|
44
|
+
errors: readonly OutputUnit[];
|
|
45
|
+
}, _options?: Record<string, never>): string;
|
|
46
|
+
//# sourceMappingURL=tool-arguments.d.ts.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** True for a plain JSON object (not `null`, not an array). */
|
|
2
|
+
export function isRecord(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Renames each alias present in `args` to its canonical key, in place. An alias whose canonical key
|
|
7
|
+
* already holds a different value is ambiguous and refused; an identical value collapses silently.
|
|
8
|
+
* @throws Error naming the alias to rename.
|
|
9
|
+
*/
|
|
10
|
+
function applyAliases({ args, aliases }) {
|
|
11
|
+
for (const [alias, canonical] of Object.entries(aliases)) {
|
|
12
|
+
if (!Object.hasOwn(args, alias))
|
|
13
|
+
continue;
|
|
14
|
+
if (Object.hasOwn(args, canonical) && args[canonical] !== args[alias]) {
|
|
15
|
+
throw new Error(`rename \`${alias}\` to \`${canonical}\` and supply only one value for \`${canonical}\``);
|
|
16
|
+
}
|
|
17
|
+
args[canonical] = args[alias];
|
|
18
|
+
delete args[alias];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Moves `extraKeys` from the top level of `args` into its `input` object (created when absent), in place.
|
|
23
|
+
* @throws Error when `input` is not a JSON object, or already holds one of the keys being folded.
|
|
24
|
+
*/
|
|
25
|
+
function foldExtraKeysIntoInput({ args, extraKeys }) {
|
|
26
|
+
const input = Object.hasOwn(args, 'input') ? args.input : {};
|
|
27
|
+
if (!isRecord(input)) {
|
|
28
|
+
throw new Error("put the tool's own arguments inside `input`; replace `input` with a JSON object");
|
|
29
|
+
}
|
|
30
|
+
if (extraKeys.some((key) => Object.hasOwn(input, key))) {
|
|
31
|
+
throw new Error("put the tool's own arguments inside `input`");
|
|
32
|
+
}
|
|
33
|
+
args.input = { ...input, ...Object.fromEntries(extraKeys.map((key) => [key, args[key]])) };
|
|
34
|
+
for (const key of extraKeys)
|
|
35
|
+
delete args[key];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* For a strict flat schema (no additional or pattern properties, no declared `input`), refuses a
|
|
39
|
+
* nested `input` object carrying a key the schema declares at the top level, with a move instruction.
|
|
40
|
+
* Any other shape is left for schema validation to report.
|
|
41
|
+
* @throws Error naming the nested key to move.
|
|
42
|
+
*/
|
|
43
|
+
function assertNoMisplacedInputKey({ args, schema }) {
|
|
44
|
+
if (schema.additionalProperties !== false || schema.patternProperties)
|
|
45
|
+
return;
|
|
46
|
+
const properties = schema.properties ?? {};
|
|
47
|
+
if (!isRecord(args.input) || Object.hasOwn(properties, 'input'))
|
|
48
|
+
return;
|
|
49
|
+
const misplaced = Object.keys(args.input).find((key) => Object.hasOwn(properties, key));
|
|
50
|
+
if (misplaced !== undefined)
|
|
51
|
+
throw new Error(`move \`input.${misplaced}\` to top-level \`${misplaced}\``);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Copies caller arguments into their canonical wrapper shape before schema validation.
|
|
55
|
+
* @param requiredArgs - Tool contract and untrusted arguments; neither is mutated.
|
|
56
|
+
* @param _options - Reserved normalization options.
|
|
57
|
+
* @returns Canonical arguments; nested tool input is copied only when fields are folded.
|
|
58
|
+
* @throws Error naming an ambiguous alias, duplicate input key, or invalid wrapper field.
|
|
59
|
+
* @complexity O(k + a + i) time and space for k argument keys, a aliases, and i input keys.
|
|
60
|
+
*/
|
|
61
|
+
export function normalizeToolArguments({ tool, rawArgs }, _options = {}) {
|
|
62
|
+
if (!isRecord(rawArgs))
|
|
63
|
+
throw new Error('replace `arguments` with a JSON object');
|
|
64
|
+
const args = { ...rawArgs };
|
|
65
|
+
const properties = tool.inputSchema.properties ?? {};
|
|
66
|
+
applyAliases({ args, aliases: tool.argumentNormalization?.aliases ?? {} });
|
|
67
|
+
const extraKeys = Object.keys(args).filter((key) => !Object.hasOwn(properties, key));
|
|
68
|
+
if (tool.argumentNormalization?.foldIntoInput && extraKeys.length > 0) {
|
|
69
|
+
foldExtraKeysIntoInput({ args, extraKeys });
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
assertNoMisplacedInputKey({ args, schema: tool.inputSchema });
|
|
73
|
+
}
|
|
74
|
+
const undefinedKey = Object.keys(args).find((key) => args[key] === undefined);
|
|
75
|
+
if (undefinedKey !== undefined)
|
|
76
|
+
throw new Error(`remove \`${undefinedKey}\` or supply a JSON value instead of undefined`);
|
|
77
|
+
return args;
|
|
78
|
+
}
|
|
79
|
+
/** Turns a validator JSON Pointer (`#/a/b~1c`) into a dotted field path (`a.b/c`); the root becomes `''`. */
|
|
80
|
+
function pointerToFieldPath({ instanceLocation }) {
|
|
81
|
+
return instanceLocation.replace(/^#\/?/, '').split('/').map((part) => part.replace(/~1/g, '/').replace(/~0/g, '~')).join('.');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The supply/remove instruction for a `required` or `additionalProperties` error, whose message
|
|
85
|
+
* quotes the offending property name; `undefined` for any other error, or when no name is quoted.
|
|
86
|
+
*/
|
|
87
|
+
function namedFieldMessage({ error, path }) {
|
|
88
|
+
if (error.keyword !== 'required' && error.keyword !== 'additionalProperties')
|
|
89
|
+
return undefined;
|
|
90
|
+
const field = error.error.match(/"(.*)"/)?.[1];
|
|
91
|
+
if (field === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
const key = path ? `${path}.${field}` : field;
|
|
94
|
+
return error.keyword === 'required' ? `supply required argument \`${key}\`` : `remove unknown argument \`${key}\``;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Formats structured validation errors as field-specific recovery instructions.
|
|
98
|
+
* @param requiredArgs - Validator errors, including JSON Pointer locations and keywords.
|
|
99
|
+
* @param _options - Reserved formatting options.
|
|
100
|
+
* @returns One actionable diagnostic; no schema-location trace.
|
|
101
|
+
* @complexity O(e + p) for e errors and p characters in the selected field path.
|
|
102
|
+
*/
|
|
103
|
+
export function argumentShapeMessage({ errors }, _options = {}) {
|
|
104
|
+
// Keep the additionalProperties diagnostic ahead of its generic false-schema child.
|
|
105
|
+
const error = errors.find((entry) => entry.keyword === 'required' || entry.keyword === 'additionalProperties' || entry.instanceLocation !== '#') ?? errors[0];
|
|
106
|
+
if (error === undefined)
|
|
107
|
+
return 'supply arguments matching the declared input schema';
|
|
108
|
+
const path = pointerToFieldPath({ instanceLocation: error.instanceLocation });
|
|
109
|
+
return namedFieldMessage({ error, path }) ?? `change \`${path || 'arguments'}\`: ${error.error}`;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=tool-arguments.js.map
|
|
@@ -1,6 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @jini-ai/mcp/server/tool-protocol
|
|
3
|
+
*
|
|
4
|
+
* The pure, SDK-connection-free half of the MCP tool-hosting mechanism: the
|
|
5
|
+
* `McpToolDef` contract a caller registers tools against, `tools/list`
|
|
6
|
+
* projection, and `tools/call` dispatch (look up a tool by name, run its
|
|
7
|
+
* handler, wrap the result). None of this touches `@modelcontextprotocol/sdk`'s
|
|
8
|
+
* `Server`/transport classes or the network — a tool `handler` is just
|
|
9
|
+
* `({ args, ctx }) => value | Promise<value>` that either returns a
|
|
10
|
+
* JSON-serializable payload (wrapped as a successful MCP result) or throws
|
|
11
|
+
* (wrapped as an `{isError:true}` MCP result). `./tool-server.js` is the thin
|
|
12
|
+
* layer that wires this to a real `Server` + `StdioServerTransport`.
|
|
13
|
+
*
|
|
14
|
+
* ## The one exception to "wrapped as a successful MCP result"
|
|
15
|
+
*
|
|
16
|
+
* {@link okResult} used to JSON.stringify every non-string payload unconditionally, which is exactly
|
|
17
|
+
* right for the overwhelming majority of handlers (plain JSON — an id, a list, a count) but wrong
|
|
18
|
+
* for a handler whose payload IS already a well-formed MCP content envelope (`{content: [...]}` of
|
|
19
|
+
* recognized block types). Stringifying that flattens a typed `image` block into an inert
|
|
20
|
+
* base64-in-quotes string the client can no longer render or hand to a vision model — the exact bug
|
|
21
|
+
* `execute_delegated_tool` (`../tools/delegated-tool.js`) hit: `@jini-ai/daemon`'s
|
|
22
|
+
* `delegated-tool-bridge.ts` already keeps an image block in its returned `ToolExecutionResult.output`
|
|
23
|
+
* (it is model-safe per that package's `tool-result-surfaces.ts`), but this module still turned it
|
|
24
|
+
* back into text on the way out. It is also how a **human-confirmation token reached model-visible
|
|
25
|
+
* text** in a separate, traced incident ( Decision 5): a `resource` block carrying the token
|
|
26
|
+
* fell to the same stringify path because an earlier version of this fix only recognized `text` and
|
|
27
|
+
* `image`, and `resource` was not yet in that hand-rolled allowlist.
|
|
28
|
+
*
|
|
29
|
+
* {@link okResult} now recognizes a `content` array whose every entry validates against the pinned
|
|
30
|
+
* `@modelcontextprotocol/sdk`'s own `ContentBlockSchema` — the same union the SDK itself defines
|
|
31
|
+
* `CallToolResultSchema.content` against — and passes such an envelope through verbatim instead of
|
|
32
|
+
* stringifying it. Deferring to the SDK's schema rather than re-implementing a per-type shape check
|
|
33
|
+
* here means every block type the pinned protocol version defines (`text`, `image`, `audio`,
|
|
34
|
+
* `resource`, `resource_link` as of `@modelcontextprotocol/sdk@^1.29.0`) is recognized automatically,
|
|
35
|
+
* and a future SDK upgrade that adds another block type is picked up the same way, without another
|
|
36
|
+
* hand-edit to this module. Whitelist, not blacklist: a `content` array holding even one entry that
|
|
37
|
+
* does not validate falls back to the stringify path, so a malformed block is never forwarded as if it
|
|
38
|
+
* were valid protocol output. What this does **not** guarantee: a block type the pinned SDK does not
|
|
39
|
+
* yet know about (a real future MCP protocol addition ahead of this dependency) still falls back to
|
|
40
|
+
* stringify until the SDK dependency itself is upgraded — this fix converts "remember to update this
|
|
41
|
+
* allowlist" into "remember to bump the SDK", not into an unconditional guarantee. It also does not
|
|
42
|
+
* (and cannot) judge intent: a handler that puts a secret inside an otherwise well-formed `text` block
|
|
43
|
+
* will still have it pass through as designed — withholding a value from the model is the handler's
|
|
44
|
+
* responsibility ( Decision 4), not this generic wrapper's.
|
|
45
|
+
* See docs/decisions/DR-002-protocol-resource-boundary.md.
|
|
46
|
+
*/
|
|
47
|
+
import { type ToolDescriptor } from '@jini-ai/core';
|
|
1
48
|
import type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
49
|
+
import { type McpToolArgumentContract } from './tool-arguments.js';
|
|
2
50
|
/** What every tool handler receives alongside its parsed arguments. */
|
|
3
51
|
export interface McpToolContext {
|
|
52
|
+
/** Publishes a successfully described, host-owned descriptor in this server run only. */
|
|
53
|
+
readonly publishTool?: (required: {
|
|
54
|
+
descriptor: ToolDescriptor;
|
|
55
|
+
}, optional?: Record<string, never>) => Promise<void>;
|
|
56
|
+
/** Host registration metadata, supplied outside model-controlled arguments. */
|
|
57
|
+
readonly diagnosticToolIds?: readonly string[];
|
|
4
58
|
/** The resolved daemon HTTP base URL (no trailing slash), fixed for the lifetime of one server run. */
|
|
5
59
|
readonly baseUrl: string;
|
|
6
60
|
/** Defaults to the global `fetch`; threaded through so a host can inject its own (e.g. for tests). */
|
|
@@ -42,6 +96,7 @@ export declare function daemonCallOptions({ ctx }: {
|
|
|
42
96
|
fetchImpl: typeof fetch;
|
|
43
97
|
headers?: Record<string, string>;
|
|
44
98
|
signal?: AbortSignal;
|
|
99
|
+
diagnosticToolIds?: readonly string[];
|
|
45
100
|
};
|
|
46
101
|
/**
|
|
47
102
|
* One MCP tool a `createMcpToolServer` caller registers. `name` must be unique within a given
|
|
@@ -51,10 +106,9 @@ export declare function daemonCallOptions({ ctx }: {
|
|
|
51
106
|
* `CallToolResult` shape by {@link handleToolCall}, so individual tools never construct MCP
|
|
52
107
|
* protocol objects themselves.
|
|
53
108
|
*/
|
|
54
|
-
export interface McpToolDef<Args extends Record<string, unknown> = Record<string, unknown>> {
|
|
109
|
+
export interface McpToolDef<Args extends Record<string, unknown> = Record<string, unknown>> extends McpToolArgumentContract {
|
|
55
110
|
readonly name: string;
|
|
56
111
|
readonly description: string;
|
|
57
|
-
readonly inputSchema: Tool['inputSchema'];
|
|
58
112
|
readonly annotations?: Tool['annotations'];
|
|
59
113
|
readonly handler: (requiredArgs: {
|
|
60
114
|
args: Args;
|
|
@@ -62,7 +116,7 @@ export interface McpToolDef<Args extends Record<string, unknown> = Record<string
|
|
|
62
116
|
}) => Promise<unknown> | unknown;
|
|
63
117
|
}
|
|
64
118
|
/**
|
|
65
|
-
* Wraps a
|
|
119
|
+
* Wraps a tool result as an MCP `CallToolResult`, preserving explicit errors and failed batch outcomes.
|
|
66
120
|
*
|
|
67
121
|
* A payload that is already a well-formed MCP content envelope (see {@link isMcpContentEnvelope}) is
|
|
68
122
|
* returned with its `content` array verbatim, so a typed block inside it — `image`, `resource`, or any
|
|
@@ -72,7 +126,7 @@ export interface McpToolDef<Args extends Record<string, unknown> = Record<string
|
|
|
72
126
|
* JSON-stringified (compact, no indentation) into one.
|
|
73
127
|
*
|
|
74
128
|
* @complexity O(n) in the number of content-array entries, only when `payload` already looks like an
|
|
75
|
-
* envelope; O(1) otherwise.
|
|
129
|
+
* envelope or batch; O(1) otherwise.
|
|
76
130
|
*/
|
|
77
131
|
export declare function okResult({ payload }: {
|
|
78
132
|
payload: unknown;
|
|
@@ -95,7 +149,8 @@ export declare function buildToolIndex({ tools }: {
|
|
|
95
149
|
tools: readonly McpToolDef[];
|
|
96
150
|
}): Map<string, McpToolDef>;
|
|
97
151
|
/**
|
|
98
|
-
* `tools/call` dispatch: looks up `name` in `tools`,
|
|
152
|
+
* `tools/call` dispatch: looks up `name` in `tools`, normalizes aliases/wrapper input, then
|
|
153
|
+
* validates the canonical arguments against the tool's
|
|
99
154
|
* declared `inputSchema`, runs its handler, and converts the outcome to a `CallToolResult` — an
|
|
100
155
|
* unknown tool name, a schema violation, or a thrown error all produce an `{isError:true}` result
|
|
101
156
|
* rather than rejecting, matching MCP's convention that tool failures are protocol-level results,
|
|
@@ -103,8 +158,8 @@ export declare function buildToolIndex({ tools }: {
|
|
|
103
158
|
* agent, not this repo's own code) supplies `rawArgs`, so a handler must never be trusted to check
|
|
104
159
|
* its own declared contract on the untrusted-input path. Both a validation failure's message and a
|
|
105
160
|
* thrown error's message are passed through {@link sanitizeUntrustedText} before reaching the
|
|
106
|
-
* result: either can end up echoing caller- or daemon-supplied text
|
|
107
|
-
*
|
|
161
|
+
* result: either can end up echoing caller- or daemon-supplied text. The validation prefix uses the
|
|
162
|
+
* registered tool name outside sanitization so long gateway names are not mistaken for secrets.
|
|
108
163
|
*/
|
|
109
164
|
export declare function handleToolCall({ name, tools, ctx }: {
|
|
110
165
|
name: string;
|
|
@@ -44,9 +44,11 @@
|
|
|
44
44
|
* responsibility ( Decision 4), not this generic wrapper's.
|
|
45
45
|
* See docs/decisions/DR-002-protocol-resource-boundary.md.
|
|
46
46
|
*/
|
|
47
|
+
import { batchOutcomeFor } from '@jini-ai/core';
|
|
47
48
|
import { sanitizeUntrustedText } from '@jini-ai/core/text';
|
|
48
|
-
import {
|
|
49
|
+
import { Validator } from '@cfworker/json-schema';
|
|
49
50
|
import { ContentBlockSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
51
|
+
import { argumentShapeMessage, isRecord, normalizeToolArguments } from './tool-arguments.js';
|
|
50
52
|
/**
|
|
51
53
|
* Maps a tool context onto the `DaemonRequestOptions` every daemon call needs.
|
|
52
54
|
*
|
|
@@ -64,13 +66,11 @@ import { ContentBlockSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
|
64
66
|
export function daemonCallOptions({ ctx }) {
|
|
65
67
|
return {
|
|
66
68
|
fetchImpl: ctx.fetchImpl,
|
|
69
|
+
...(ctx.diagnosticToolIds !== undefined ? { diagnosticToolIds: ctx.diagnosticToolIds } : {}),
|
|
67
70
|
...(ctx.authHeaders !== undefined ? { headers: { ...ctx.authHeaders } } : {}),
|
|
68
71
|
...(ctx.signal !== undefined ? { signal: ctx.signal } : {}),
|
|
69
72
|
};
|
|
70
73
|
}
|
|
71
|
-
function isRecord(value) {
|
|
72
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
73
|
-
}
|
|
74
74
|
/**
|
|
75
75
|
* True when `block` is a well-formed MCP content block this module will pass through verbatim rather
|
|
76
76
|
* than JSON-stringify. Delegates to the pinned `@modelcontextprotocol/sdk`'s own `ContentBlockSchema`
|
|
@@ -101,7 +101,7 @@ function isMcpContentEnvelope(payload) {
|
|
|
101
101
|
return Array.isArray(content) && content.every(isPassthroughContentBlock);
|
|
102
102
|
}
|
|
103
103
|
/**
|
|
104
|
-
* Wraps a
|
|
104
|
+
* Wraps a tool result as an MCP `CallToolResult`, preserving explicit errors and failed batch outcomes.
|
|
105
105
|
*
|
|
106
106
|
* A payload that is already a well-formed MCP content envelope (see {@link isMcpContentEnvelope}) is
|
|
107
107
|
* returned with its `content` array verbatim, so a typed block inside it — `image`, `resource`, or any
|
|
@@ -111,17 +111,20 @@ function isMcpContentEnvelope(payload) {
|
|
|
111
111
|
* JSON-stringified (compact, no indentation) into one.
|
|
112
112
|
*
|
|
113
113
|
* @complexity O(n) in the number of content-array entries, only when `payload` already looks like an
|
|
114
|
-
* envelope; O(1) otherwise.
|
|
114
|
+
* envelope or batch; O(1) otherwise.
|
|
115
115
|
*/
|
|
116
116
|
export function okResult({ payload }) {
|
|
117
|
+
const outcome = batchOutcomeFor({ output: payload });
|
|
118
|
+
const isError = (typeof payload === 'object' && payload !== null && 'isError' in payload && payload.isError === true)
|
|
119
|
+
|| outcome === 'partial_failure' || outcome === 'error';
|
|
117
120
|
if (isMcpContentEnvelope(payload)) {
|
|
118
|
-
return { content: payload.content };
|
|
121
|
+
return { content: payload.content, ...(isError ? { isError: true } : {}) };
|
|
119
122
|
}
|
|
120
123
|
// Compact on the wire: indentation made nested results ~2.5x larger for the model to read. A view
|
|
121
124
|
// that shows a result to a person re-indents it at render time (`@jini-ai/chat`'s
|
|
122
125
|
// `formatToolOutputForDisplay`).
|
|
123
126
|
const text = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
124
|
-
return { content: [{ type: 'text', text }] };
|
|
127
|
+
return { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) };
|
|
125
128
|
}
|
|
126
129
|
/** Wraps a tool failure as an MCP `isError` result. */
|
|
127
130
|
export function errorResult({ message }) {
|
|
@@ -154,31 +157,22 @@ export function buildToolIndex({ tools }) {
|
|
|
154
157
|
}
|
|
155
158
|
return index;
|
|
156
159
|
}
|
|
157
|
-
/**
|
|
158
|
-
* The SDK's own edge-runtime-compatible (no `eval`/codegen) JSON Schema validator — the same
|
|
159
|
-
* mechanism `McpServer.registerTool()`'s high-level helper uses internally, borrowed here because
|
|
160
|
-
* `createMcpToolServer` is built on the SDK's low-level `Server.setRequestHandler` instead (see
|
|
161
|
-
* `tool-server.ts`'s module doc), which does not validate a tool's declared `inputSchema` against
|
|
162
|
-
* arguments before its handler runs. One provider instance is reused across every tool: it holds no
|
|
163
|
-
* per-schema state itself (see {@link compiledValidators} for the actual compiled-validator cache).
|
|
164
|
-
*/
|
|
165
|
-
const schemaValidatorProvider = new CfWorkerJsonSchemaValidator();
|
|
166
|
-
/**
|
|
167
|
-
* Compiling a validator from a schema is real work `CfWorkerJsonSchemaValidator` does not cache
|
|
168
|
-
* internally (per its own doc), so it is cached here per `McpToolDef` instance — stable for a
|
|
169
|
-
* server's whole lifetime, since tool defs are created once at module load and never rebuilt.
|
|
170
|
-
*/
|
|
160
|
+
/** Compiled schemas are reused for each stable tool definition, as in the SDK provider. */
|
|
171
161
|
const compiledValidators = new WeakMap();
|
|
162
|
+
/** Returns the cached validator, retaining structured field locations for recovery instructions. */
|
|
172
163
|
function validatorForTool(tool) {
|
|
173
164
|
const cached = compiledValidators.get(tool);
|
|
174
165
|
if (cached)
|
|
175
166
|
return cached;
|
|
176
|
-
|
|
167
|
+
// The validator adds internal URI fields while compiling. F0's published schemas are frozen;
|
|
168
|
+
// compile a detached copy so validation cannot mutate the immutable wire contract.
|
|
169
|
+
const validator = new Validator(structuredClone(tool.inputSchema), '2020-12', true);
|
|
177
170
|
compiledValidators.set(tool, validator);
|
|
178
171
|
return validator;
|
|
179
172
|
}
|
|
180
173
|
/**
|
|
181
|
-
* `tools/call` dispatch: looks up `name` in `tools`,
|
|
174
|
+
* `tools/call` dispatch: looks up `name` in `tools`, normalizes aliases/wrapper input, then
|
|
175
|
+
* validates the canonical arguments against the tool's
|
|
182
176
|
* declared `inputSchema`, runs its handler, and converts the outcome to a `CallToolResult` — an
|
|
183
177
|
* unknown tool name, a schema violation, or a thrown error all produce an `{isError:true}` result
|
|
184
178
|
* rather than rejecting, matching MCP's convention that tool failures are protocol-level results,
|
|
@@ -186,34 +180,30 @@ function validatorForTool(tool) {
|
|
|
186
180
|
* agent, not this repo's own code) supplies `rawArgs`, so a handler must never be trusted to check
|
|
187
181
|
* its own declared contract on the untrusted-input path. Both a validation failure's message and a
|
|
188
182
|
* thrown error's message are passed through {@link sanitizeUntrustedText} before reaching the
|
|
189
|
-
* result: either can end up echoing caller- or daemon-supplied text
|
|
190
|
-
*
|
|
183
|
+
* result: either can end up echoing caller- or daemon-supplied text. The validation prefix uses the
|
|
184
|
+
* registered tool name outside sanitization so long gateway names are not mistaken for secrets.
|
|
191
185
|
*/
|
|
192
186
|
export async function handleToolCall({ name, tools, ctx }, { rawArgs } = {}) {
|
|
187
|
+
const diagnosticToolIds = [...tools.keys(), ...(ctx.diagnosticToolIds ?? [])];
|
|
188
|
+
ctx = { ...ctx, diagnosticToolIds };
|
|
193
189
|
const tool = tools.get(name);
|
|
194
190
|
if (tool === undefined) {
|
|
195
191
|
return errorResult({ message: `unknown tool: ${name}` });
|
|
196
192
|
}
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
// validator does not only return `{valid:false}` for a bad argument — `@cfworker/json-schema`
|
|
200
|
-
// *throws* for JavaScript values JSON cannot encode (an explicitly-`undefined` optional
|
|
201
|
-
// property yields `Instances of "undefined" type are not supported.`). Compiling the
|
|
202
|
-
// validator can throw too, on a malformed `inputSchema`. Either escaping would break this
|
|
203
|
-
// function's whole contract, that a schema violation is an `{isError:true}` result and never
|
|
204
|
-
// a rejection — and `handleToolCall` is exported, typed `Record<string, unknown>`, and
|
|
205
|
-
// callable directly by a host, so this is reachable and not merely theoretical.
|
|
193
|
+
// Normalization and schema validation share an error boundary; handlers run only afterward.
|
|
194
|
+
// See docs/decisions/DR-005-proxy-argument-shapes.md.
|
|
206
195
|
let validatedArgs;
|
|
207
196
|
try {
|
|
208
|
-
const
|
|
197
|
+
const args = normalizeToolArguments({ tool, rawArgs: rawArgs ?? {} });
|
|
198
|
+
const validation = validatorForTool(tool).validate(args);
|
|
209
199
|
if (!validation.valid) {
|
|
210
|
-
return errorResult({ message:
|
|
200
|
+
return errorResult({ message: `invalid arguments for ${tool.name}: ${sanitizeUntrustedText({ text: argumentShapeMessage({ errors: validation.errors }) }, { diagnosticToolIds })}` });
|
|
211
201
|
}
|
|
212
|
-
validatedArgs =
|
|
202
|
+
validatedArgs = args;
|
|
213
203
|
}
|
|
214
204
|
catch (err) {
|
|
215
205
|
const message = err instanceof Error ? err.message : String(err);
|
|
216
|
-
return errorResult({ message:
|
|
206
|
+
return errorResult({ message: `invalid arguments for ${tool.name}: ${sanitizeUntrustedText({ text: message }, { diagnosticToolIds })}` });
|
|
217
207
|
}
|
|
218
208
|
try {
|
|
219
209
|
const result = await tool.handler({ args: validatedArgs, ctx: ctx });
|
|
@@ -221,7 +211,7 @@ export async function handleToolCall({ name, tools, ctx }, { rawArgs } = {}) {
|
|
|
221
211
|
}
|
|
222
212
|
catch (err) {
|
|
223
213
|
const message = err instanceof Error ? err.message : String(err);
|
|
224
|
-
return errorResult({ message: sanitizeUntrustedText({ text: message }) });
|
|
214
|
+
return errorResult({ message: sanitizeUntrustedText({ text: message }, { diagnosticToolIds }) });
|
|
225
215
|
}
|
|
226
216
|
}
|
|
227
217
|
//# sourceMappingURL=tool-protocol.js.map
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* with nominal private fields; default adapters translate those contracts to the real SDK.
|
|
8
8
|
*/
|
|
9
9
|
import type { Readable, Writable } from 'node:stream';
|
|
10
|
+
import { type ToolDescriptor } from '@jini-ai/core';
|
|
10
11
|
import type { McpTransportLike } from './ports.js';
|
|
11
12
|
export type { McpTransportLike } from './ports.js';
|
|
12
13
|
import type { ServerOptions } from '@modelcontextprotocol/sdk/server/index.js';
|
|
@@ -14,6 +15,7 @@ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSche
|
|
|
14
15
|
import type { CallToolResult, Implementation, ListResourcesResult, ListToolsResult, ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';
|
|
15
16
|
import { type McpResourceDef } from './resource-protocol.js';
|
|
16
17
|
import { type McpToolDef } from './tool-protocol.js';
|
|
18
|
+
import { type CreateExecuteDelegatedToolToolOptions } from './tools/delegated-tool.js';
|
|
17
19
|
/** Object-shaped server port. The default adapter translates it to the SDK interface. */
|
|
18
20
|
export interface McpServerLike {
|
|
19
21
|
setRequestHandler(args: {
|
|
@@ -52,6 +54,8 @@ export interface McpServerLike {
|
|
|
52
54
|
connect(args: {
|
|
53
55
|
transport: McpTransportLike;
|
|
54
56
|
}): Promise<void>;
|
|
57
|
+
/** Optional for static embeddings; required when delegated publication is enabled. */
|
|
58
|
+
sendToolListChanged?(required: Record<string, never>, optional?: Record<string, never>): Promise<void>;
|
|
55
59
|
}
|
|
56
60
|
/** Required identity, tool surface, and daemon URL resolver. */
|
|
57
61
|
export interface McpToolServerRequiredArgs {
|
|
@@ -64,6 +68,10 @@ export interface McpToolServerRequiredArgs {
|
|
|
64
68
|
readonly resolveBaseUrl: () => Promise<string> | string;
|
|
65
69
|
}
|
|
66
70
|
export interface McpToolServerOptions {
|
|
71
|
+
/** Opt in to bounded, per-run typed publication after describe_tool. */
|
|
72
|
+
readonly delegatedTools?: McpDelegatedToolsOptions;
|
|
73
|
+
/** Trusted daemon registration IDs carried into every error sanitizer. */
|
|
74
|
+
readonly diagnosticToolIds?: readonly string[];
|
|
67
75
|
/** The bounded, explicit read-only resource list this server hosts. Duplicate uris throw at construction time. Omit (or pass an empty array) for a tools-only server — `capabilities.resources` is only advertised when this is non-empty. */
|
|
68
76
|
readonly resources?: readonly McpResourceDef[];
|
|
69
77
|
/** Free-text guidance surfaced to the MCP client alongside the tool list. Optional — omit for a caller with nothing to add beyond each tool's own `description`. */
|
|
@@ -93,6 +101,17 @@ export interface McpToolServerOptions {
|
|
|
93
101
|
stdout?: Writable;
|
|
94
102
|
}) => McpTransportLike;
|
|
95
103
|
}
|
|
104
|
+
/** The admitted host descriptors supplied here must come from the existing registry/catalog.
|
|
105
|
+
* Publication never connects to a federated server or admits its upstream list changes.
|
|
106
|
+
*/
|
|
107
|
+
export interface McpDelegatedToolsOptions extends CreateExecuteDelegatedToolToolOptions {
|
|
108
|
+
readonly runId: string;
|
|
109
|
+
/** Resolve these through the authenticated daemon before running a client without refresh. */
|
|
110
|
+
readonly initialTools?: readonly ToolDescriptor[];
|
|
111
|
+
/** Defaults to F0's 64 tools and 256 KiB of schemas; new entries are skipped at the cap. */
|
|
112
|
+
readonly maxTools?: number;
|
|
113
|
+
readonly maxSchemaBytes?: number;
|
|
114
|
+
}
|
|
96
115
|
export interface McpToolServerHandle {
|
|
97
116
|
/**
|
|
98
117
|
* Resolves the daemon base URL, connects the MCP server to its transport, and holds the process
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createToolExposure } from '@jini-ai/core';
|
|
1
2
|
import { adaptSdkTransport, resolveSdkTransport } from './sdk-transport.js';
|
|
2
3
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -5,6 +6,7 @@ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSche
|
|
|
5
6
|
import { createMcpIdleExitController } from '../client/client.js';
|
|
6
7
|
import { buildResourceIndex, handleResourceRead, resourcesToList } from './resource-protocol.js';
|
|
7
8
|
import { buildToolIndex, handleToolCall, toolsToList } from './tool-protocol.js';
|
|
9
|
+
import { createTypedDelegatedToolTool } from './tools/delegated-tool.js';
|
|
8
10
|
/** Auto-exit an idle server after this long without tool activity. */
|
|
9
11
|
const DEFAULT_IDLE_MS = 30 * 60 * 1000;
|
|
10
12
|
// SDK objects remain behind these adapters; hosts implement the object-shaped ports above.
|
|
@@ -13,11 +15,84 @@ function defaultCreateServer({ info, options }) {
|
|
|
13
15
|
return {
|
|
14
16
|
setRequestHandler: (({ schema, handler }) => server.setRequestHandler(schema, (request, extra) => handler({ request }, { extra }))),
|
|
15
17
|
connect: ({ transport }) => server.connect(resolveSdkTransport({ transport })),
|
|
18
|
+
sendToolListChanged: () => server.sendToolListChanged(),
|
|
16
19
|
};
|
|
17
20
|
}
|
|
18
21
|
function defaultCreateTransport(_requiredArgs, { stdin, stdout } = {}) {
|
|
19
22
|
return adaptSdkTransport({ transport: new StdioServerTransport(stdin, stdout) });
|
|
20
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* The tool context shared by every call in one run: the resolved daemon URL without its trailing
|
|
26
|
+
* slash (coerced to a string for JS hosts that ignore the contract), the configured or global
|
|
27
|
+
* `fetch`, and the auth headers and diagnostic IDs only when configured.
|
|
28
|
+
*/
|
|
29
|
+
function buildToolContext({ resolvedBaseUrl }, options) {
|
|
30
|
+
return {
|
|
31
|
+
...(options.diagnosticToolIds !== undefined ? { diagnosticToolIds: options.diagnosticToolIds } : {}),
|
|
32
|
+
baseUrl: String(resolvedBaseUrl).replace(/\/$/, ''),
|
|
33
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
34
|
+
...(options.authHeaders !== undefined ? { authHeaders: options.authHeaders } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** The stdio pair handed to `createTransport`, carrying only the streams the caller configured. */
|
|
38
|
+
function stdioOptions(options) {
|
|
39
|
+
return {
|
|
40
|
+
...(options.stdin === undefined ? {} : { stdin: options.stdin }),
|
|
41
|
+
...(options.stdout === undefined ? {} : { stdout: options.stdout }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Own only the MCP projection/dispatch index. F0 owns immutable exposure, and the daemon catalog
|
|
46
|
+
* supplies each descriptor; the temporary source below is not another definition catalog.
|
|
47
|
+
* @returns An install-before-notify publisher, or undefined for a static server.
|
|
48
|
+
* @throws On malformed schemas, invalid exposure limits, or an embedding without notification support.
|
|
49
|
+
* @complexity O(n + s) per promotion for n bounded exposed tools and s schema bytes; no daemon I/O.
|
|
50
|
+
*/
|
|
51
|
+
function createToolPublisher({ index, server, options }) {
|
|
52
|
+
if (!options)
|
|
53
|
+
return undefined;
|
|
54
|
+
const notify = server.sendToolListChanged?.bind(server);
|
|
55
|
+
if (!notify)
|
|
56
|
+
throw new Error('delegated publication requires tools/list_changed support');
|
|
57
|
+
let descriptors = options.initialTools ?? [];
|
|
58
|
+
const exposure = createToolExposure({ registry: { list: () => descriptors } }, {
|
|
59
|
+
reservedNames: [...index.keys()],
|
|
60
|
+
...(options.maxTools === undefined ? {} : { maxTools: options.maxTools }),
|
|
61
|
+
...(options.maxSchemaBytes === undefined ? {} : { maxSchemaBytes: options.maxSchemaBytes }),
|
|
62
|
+
});
|
|
63
|
+
// Validate before promotion: malformed descriptors must not consume a pinned name or budget.
|
|
64
|
+
const validate = (descriptor) => createTypedDelegatedToolTool({ runId: options.runId, tool: { wireName: descriptor.id, descriptor } }, options);
|
|
65
|
+
for (const descriptor of descriptors)
|
|
66
|
+
validate(descriptor);
|
|
67
|
+
const initial = exposure.promote({ toolIds: descriptors.map(descriptor => descriptor.id) });
|
|
68
|
+
for (const tool of initial.snapshot.tools)
|
|
69
|
+
index.set(tool.wireName, createTypedDelegatedToolTool({ runId: options.runId, tool }, options));
|
|
70
|
+
descriptors = [];
|
|
71
|
+
let notificationPending = false;
|
|
72
|
+
return async ({ descriptor }, _optional = {}) => {
|
|
73
|
+
validate(descriptor);
|
|
74
|
+
descriptors = [descriptor];
|
|
75
|
+
const delta = exposure.promote({ toolIds: [descriptor.id] });
|
|
76
|
+
descriptors = [];
|
|
77
|
+
for (const tool of delta.snapshot.tools) {
|
|
78
|
+
if (!index.has(tool.wireName))
|
|
79
|
+
index.set(tool.wireName, createTypedDelegatedToolTool({ runId: options.runId, tool }, options));
|
|
80
|
+
}
|
|
81
|
+
if (delta.added.length > 0)
|
|
82
|
+
notificationPending = true;
|
|
83
|
+
if (!notificationPending)
|
|
84
|
+
return;
|
|
85
|
+
// Clear before awaiting: a concurrent promotion must retain its own unsent notification.
|
|
86
|
+
notificationPending = false;
|
|
87
|
+
try {
|
|
88
|
+
await notify({});
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
notificationPending = true;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
21
96
|
/**
|
|
22
97
|
* Builds a stdio MCP server hosting `requiredArgs.tools` (and, optionally, `options.resources`).
|
|
23
98
|
* Construction is synchronous and cheap (validates tool-name uniqueness via
|
|
@@ -26,35 +101,35 @@ function defaultCreateTransport(_requiredArgs, { stdin, stdout } = {}) {
|
|
|
26
101
|
* handle's `run()`.
|
|
27
102
|
*/
|
|
28
103
|
export function createMcpToolServer(requiredArgs, options = {}) {
|
|
29
|
-
|
|
104
|
+
// Validate eagerly, but keep the mutable dispatch index inside run() so re-runs are isolated.
|
|
105
|
+
buildToolIndex({ tools: requiredArgs.tools });
|
|
30
106
|
const resources = options.resources ?? [];
|
|
31
107
|
const resourceIndex = buildResourceIndex({ resources });
|
|
32
108
|
const createServer = options.createServer ?? defaultCreateServer;
|
|
33
109
|
const createTransport = options.createTransport ?? defaultCreateTransport;
|
|
34
110
|
return {
|
|
35
111
|
async run(_required) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
baseUrl: String(resolvedBaseUrl).replace(/\/$/, ''),
|
|
39
|
-
fetchImpl: options.fetchImpl ?? fetch,
|
|
40
|
-
...(options.authHeaders !== undefined ? { authHeaders: options.authHeaders } : {}),
|
|
41
|
-
};
|
|
42
|
-
let closeTransportForIdle = null;
|
|
43
|
-
const idleExit = createMcpIdleExitController({
|
|
44
|
-
idleMs: options.idleMs ?? DEFAULT_IDLE_MS,
|
|
45
|
-
onIdle: () => closeTransportForIdle?.(),
|
|
46
|
-
});
|
|
47
|
-
const withActivity = (handler) => (...args) => idleExit.trackRequest({ fn: () => handler(...args) });
|
|
112
|
+
const toolIndex = buildToolIndex({ tools: requiredArgs.tools });
|
|
113
|
+
let ctx = buildToolContext({ resolvedBaseUrl: await requiredArgs.resolveBaseUrl() }, options);
|
|
48
114
|
const server = createServer({
|
|
49
115
|
info: { name: requiredArgs.name, version: requiredArgs.version },
|
|
50
116
|
options: {
|
|
51
|
-
capabilities: { tools: {}, ...(resources.length > 0 ? { resources: {} } : {}) },
|
|
117
|
+
capabilities: { tools: options.delegatedTools ? { listChanged: true } : {}, ...(resources.length > 0 ? { resources: {} } : {}) },
|
|
52
118
|
...(options.instructions !== undefined ? { instructions: options.instructions } : {}),
|
|
53
119
|
},
|
|
54
120
|
});
|
|
121
|
+
const publishTool = createToolPublisher({ index: toolIndex, server, options: options.delegatedTools });
|
|
122
|
+
if (publishTool)
|
|
123
|
+
ctx = { ...ctx, publishTool };
|
|
124
|
+
let closeTransportForIdle = null;
|
|
125
|
+
const idleExit = createMcpIdleExitController({
|
|
126
|
+
idleMs: options.idleMs ?? DEFAULT_IDLE_MS,
|
|
127
|
+
onIdle: () => closeTransportForIdle?.(),
|
|
128
|
+
});
|
|
129
|
+
const withActivity = (handler) => (...args) => idleExit.trackRequest({ fn: () => handler(...args) });
|
|
55
130
|
server.setRequestHandler({
|
|
56
131
|
schema: ListToolsRequestSchema,
|
|
57
|
-
handler: withActivity(async () => ({ tools: toolsToList({ tools:
|
|
132
|
+
handler: withActivity(async () => ({ tools: toolsToList({ tools: [...toolIndex.values()] }) })),
|
|
58
133
|
});
|
|
59
134
|
server.setRequestHandler({
|
|
60
135
|
schema: CallToolRequestSchema,
|
|
@@ -70,10 +145,7 @@ export function createMcpToolServer(requiredArgs, options = {}) {
|
|
|
70
145
|
handler: withActivity(async ({ request }) => handleResourceRead({ uri: request.params.uri, resources: resourceIndex, ctx })),
|
|
71
146
|
});
|
|
72
147
|
}
|
|
73
|
-
const transport = createTransport({},
|
|
74
|
-
...(options.stdin === undefined ? {} : { stdin: options.stdin }),
|
|
75
|
-
...(options.stdout === undefined ? {} : { stdout: options.stdout }),
|
|
76
|
-
});
|
|
148
|
+
const transport = createTransport({}, stdioOptions(options));
|
|
77
149
|
try {
|
|
78
150
|
closeTransportForIdle = () => {
|
|
79
151
|
void transport.close({}).catch(() => { });
|