@jini-ai/mcp 0.3.3 → 0.4.0
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 +151 -0
- package/README.md +97 -22
- package/dist/agent-install/install.d.ts +20 -4
- package/dist/agent-install/install.js +11 -11
- package/dist/bin/serve.d.ts +25 -13
- package/dist/bin/serve.js +42 -21
- package/dist/client/client.d.ts +28 -8
- package/dist/client/client.js +25 -21
- package/dist/core/config.d.ts +57 -18
- package/dist/core/config.js +76 -48
- package/dist/core/index.d.ts +1 -2
- package/dist/core/index.js +1 -2
- package/dist/core/install-info.d.ts +4 -1
- package/dist/core/install-info.js +5 -5
- package/dist/core/tokens.d.ts +39 -13
- package/dist/core/tokens.js +54 -24
- package/dist/federation/adapter.http.d.ts +67 -0
- package/dist/federation/adapter.http.js +414 -0
- package/dist/federation/approvals/call-confirmation.d.ts +23 -0
- package/dist/federation/approvals/call-confirmation.js +124 -0
- package/dist/federation/approvals/index.d.ts +4 -0
- package/dist/federation/approvals/index.js +4 -0
- package/dist/federation/approvals/ports.d.ts +107 -0
- package/dist/federation/approvals/ports.js +2 -0
- package/dist/federation/approvals/revocation.d.ts +61 -0
- package/dist/federation/approvals/revocation.js +52 -0
- package/dist/federation/bootstrap.d.ts +100 -0
- package/dist/federation/bootstrap.js +182 -0
- package/dist/federation/config.d.ts +68 -0
- package/dist/federation/config.js +40 -0
- package/dist/federation/index.d.ts +14 -0
- package/dist/federation/index.js +14 -0
- package/dist/federation/mcp-protocol.d.ts +115 -0
- package/dist/federation/mcp-protocol.js +151 -0
- package/dist/federation/messages.d.ts +54 -0
- package/dist/federation/messages.js +35 -0
- package/dist/federation/ports.d.ts +287 -0
- package/dist/federation/ports.js +5 -0
- package/dist/federation/presets.d.ts +66 -0
- package/dist/federation/presets.js +30 -0
- package/dist/federation/refusal-notice.d.ts +198 -0
- package/dist/federation/refusal-notice.js +193 -0
- package/dist/federation/registrations.d.ts +171 -0
- package/dist/federation/registrations.js +181 -0
- package/dist/federation/reload.d.ts +112 -0
- package/dist/federation/reload.js +110 -0
- package/dist/federation/stdio/adapter.stdio.d.ts +157 -0
- package/dist/federation/stdio/adapter.stdio.js +526 -0
- package/dist/federation/stdio/default-connect.d.ts +29 -0
- package/dist/federation/stdio/default-connect.js +92 -0
- package/dist/federation/stdio/index.d.ts +4 -0
- package/dist/federation/stdio/index.js +4 -0
- package/dist/federation/stdio/stdio-launch-resolver.d.ts +139 -0
- package/dist/federation/stdio/stdio-launch-resolver.js +227 -0
- package/dist/federation/testing/adapter.memory.d.ts +177 -0
- package/dist/federation/testing/adapter.memory.js +189 -0
- package/dist/federation/testing/index.d.ts +3 -0
- package/dist/federation/testing/index.js +3 -0
- package/dist/federation/testing/tool-approvals.memory.d.ts +27 -0
- package/dist/federation/testing/tool-approvals.memory.js +38 -0
- package/dist/federation/tool-approvals.d.ts +97 -0
- package/dist/federation/tool-approvals.js +42 -0
- package/dist/federation/trust.d.ts +471 -0
- package/dist/federation/trust.js +707 -0
- package/dist/index.d.ts +175 -2
- package/dist/index.js +172 -2
- package/dist/server/daemon-client.d.ts +16 -4
- package/dist/server/daemon-client.js +41 -19
- package/dist/server/ports.d.ts +9 -0
- package/dist/server/ports.js +2 -0
- package/dist/server/resource-protocol.d.ts +14 -4
- package/dist/server/resource-protocol.js +6 -6
- package/dist/server/resources/active-resource.js +3 -3
- package/dist/server/sdk-transport.d.ts +11 -0
- package/dist/server/sdk-transport.js +31 -0
- package/dist/server/tool-protocol.d.ts +31 -9
- package/dist/server/tool-protocol.js +20 -18
- package/dist/server/tool-server.d.ts +60 -43
- package/dist/server/tool-server.js +51 -27
- package/dist/server/tools/component-catalog-tools.js +7 -7
- package/dist/server/tools/delegated-tool.d.ts +7 -4
- package/dist/server/tools/delegated-tool.js +14 -15
- package/dist/server/tools/run-tools.js +15 -15
- package/dist/server/tools/tool-catalog-tools.js +7 -7
- package/dist/tools/ask-choice/index.d.ts +123 -0
- package/dist/tools/ask-choice/index.js +98 -0
- package/dist/tools/ask-choice/pending.d.ts +11 -0
- package/dist/tools/ask-choice/pending.js +34 -0
- package/dist/tools/ask-choice/question.d.ts +16 -0
- package/dist/tools/ask-choice/question.js +60 -0
- package/dist/tools/ask-choice/schema.d.ts +91 -0
- package/dist/tools/ask-choice/schema.js +27 -0
- package/dist/tools/ask-choice/types.d.ts +106 -0
- package/dist/tools/ask-choice/types.js +2 -0
- package/package.json +43 -5
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* `ReadResourceRequestSchema` handlers, exactly as `tool-protocol.js` is
|
|
10
10
|
* wired to `ListToolsRequestSchema`/`CallToolRequestSchema`.
|
|
11
11
|
*/
|
|
12
|
-
import { sanitizeUntrustedText } from '@jini-ai/
|
|
12
|
+
import { sanitizeUntrustedText } from '@jini-ai/core/text';
|
|
13
13
|
/** Projects a resource list into the `Resource[]` shape `resources/list` returns. */
|
|
14
|
-
export function resourcesToList(resources) {
|
|
14
|
+
export function resourcesToList({ resources }) {
|
|
15
15
|
return resources.map((resource) => ({
|
|
16
16
|
uri: resource.uri,
|
|
17
17
|
name: resource.name,
|
|
@@ -20,7 +20,7 @@ export function resourcesToList(resources) {
|
|
|
20
20
|
}));
|
|
21
21
|
}
|
|
22
22
|
/** Builds a uri -> def lookup, throwing if two resources in `resources` share a uri (a caller-configuration bug, surfaced eagerly at server-construction time rather than letting the second registration shadow the first). */
|
|
23
|
-
export function buildResourceIndex(resources) {
|
|
23
|
+
export function buildResourceIndex({ resources }) {
|
|
24
24
|
const index = new Map();
|
|
25
25
|
for (const resource of resources) {
|
|
26
26
|
if (index.has(resource.uri)) {
|
|
@@ -41,18 +41,18 @@ export function buildResourceIndex(resources) {
|
|
|
41
41
|
* it leaves this function, the same conservative posture `handleToolCall` uses — a `read`
|
|
42
42
|
* implementation may re-throw text that traces back to an untrusted daemon response.
|
|
43
43
|
*/
|
|
44
|
-
export async function handleResourceRead(uri, resources, ctx) {
|
|
44
|
+
export async function handleResourceRead({ uri, resources, ctx }) {
|
|
45
45
|
const resource = resources.get(uri);
|
|
46
46
|
if (resource === undefined) {
|
|
47
47
|
throw new Error(`unsupported resource URI: ${uri}`);
|
|
48
48
|
}
|
|
49
49
|
let result;
|
|
50
50
|
try {
|
|
51
|
-
result = await resource.read(ctx);
|
|
51
|
+
result = await resource.read({ ctx: ctx });
|
|
52
52
|
}
|
|
53
53
|
catch (err) {
|
|
54
54
|
const message = err instanceof Error ? err.message : String(err);
|
|
55
|
-
throw new Error(sanitizeUntrustedText(message));
|
|
55
|
+
throw new Error(sanitizeUntrustedText({ text: message }));
|
|
56
56
|
}
|
|
57
57
|
const mimeType = result.mimeType ?? resource.mimeType;
|
|
58
58
|
return {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* tool is invoked by the model mid-conversation, while a resource can be
|
|
14
14
|
* listed and attached to context by the user/client without any tool call.
|
|
15
15
|
* This module is that second affordance for the same primitive. See
|
|
16
|
-
* `
|
|
16
|
+
* `archived provenance ledger`'s 2026-07-21 addition for why the rest of OD's resource
|
|
17
17
|
* surface (`od://skills/...`, `od://design-systems/...`) was NOT ported the
|
|
18
18
|
* same way (both require a Skill/DesignSystem noun this kernel doesn't have).
|
|
19
19
|
*
|
|
@@ -37,8 +37,8 @@ export const activeContextResource = {
|
|
|
37
37
|
name: 'Active context',
|
|
38
38
|
description: 'The resource (resourceRef) plus optional detail the caller last recorded as its current focus via POST /api/active — the same generic, product-neutral pointer the get_active_context tool proxies, exposed here as an attachable MCP resource instead of a tool call.',
|
|
39
39
|
mimeType: 'application/json',
|
|
40
|
-
read: async (ctx) => {
|
|
41
|
-
const data = await getDaemonJson(ctx.baseUrl, '/api/active', daemonCallOptions(ctx));
|
|
40
|
+
read: async ({ ctx }) => {
|
|
41
|
+
const data = await getDaemonJson({ baseUrl: ctx.baseUrl, route: '/api/active' }, daemonCallOptions({ ctx }));
|
|
42
42
|
return { text: JSON.stringify(data, null, 2) };
|
|
43
43
|
},
|
|
44
44
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
2
|
+
import type { McpTransportLike } from './ports.js';
|
|
3
|
+
/** Internal boundary adapter; SDK callbacks keep their native signature behind the port. */
|
|
4
|
+
export declare function adaptSdkTransport({ transport: sdk }: {
|
|
5
|
+
transport: Transport;
|
|
6
|
+
}): McpTransportLike;
|
|
7
|
+
/** Resolves adapters to the underlying SDK transport; host ports stay structurally usable. */
|
|
8
|
+
export declare function resolveSdkTransport({ transport }: {
|
|
9
|
+
transport: McpTransportLike;
|
|
10
|
+
}): Transport;
|
|
11
|
+
//# sourceMappingURL=sdk-transport.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const sdkTransports = new WeakMap();
|
|
2
|
+
/** Internal boundary adapter; SDK callbacks keep their native signature behind the port. */
|
|
3
|
+
export function adaptSdkTransport({ transport: sdk }) {
|
|
4
|
+
const transport = {
|
|
5
|
+
get onmessage() {
|
|
6
|
+
const handler = sdk.onmessage;
|
|
7
|
+
return handler ? ({ message }) => handler(message) : undefined;
|
|
8
|
+
},
|
|
9
|
+
set onmessage(handler) {
|
|
10
|
+
if (handler)
|
|
11
|
+
sdk.onmessage = (message) => handler({ message });
|
|
12
|
+
else
|
|
13
|
+
delete sdk.onmessage;
|
|
14
|
+
},
|
|
15
|
+
get onclose() { return sdk.onclose; },
|
|
16
|
+
set onclose(handler) {
|
|
17
|
+
if (handler)
|
|
18
|
+
sdk.onclose = handler;
|
|
19
|
+
else
|
|
20
|
+
delete sdk.onclose;
|
|
21
|
+
},
|
|
22
|
+
close: (_required) => sdk.close(),
|
|
23
|
+
};
|
|
24
|
+
sdkTransports.set(transport, sdk);
|
|
25
|
+
return transport;
|
|
26
|
+
}
|
|
27
|
+
/** Resolves adapters to the underlying SDK transport; host ports stay structurally usable. */
|
|
28
|
+
export function resolveSdkTransport({ transport }) {
|
|
29
|
+
return (sdkTransports.get(transport) ?? transport);
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=sdk-transport.js.map
|
|
@@ -36,7 +36,9 @@ export interface McpToolContext {
|
|
|
36
36
|
* @complexity O(1).
|
|
37
37
|
* @overallScore 100/100
|
|
38
38
|
*/
|
|
39
|
-
export declare function daemonCallOptions(ctx
|
|
39
|
+
export declare function daemonCallOptions({ ctx }: {
|
|
40
|
+
ctx: McpToolContext;
|
|
41
|
+
}): {
|
|
40
42
|
fetchImpl: typeof fetch;
|
|
41
43
|
headers?: Record<string, string>;
|
|
42
44
|
signal?: AbortSignal;
|
|
@@ -54,7 +56,10 @@ export interface McpToolDef<Args extends Record<string, unknown> = Record<string
|
|
|
54
56
|
readonly description: string;
|
|
55
57
|
readonly inputSchema: Tool['inputSchema'];
|
|
56
58
|
readonly annotations?: Tool['annotations'];
|
|
57
|
-
readonly handler: (
|
|
59
|
+
readonly handler: (requiredArgs: {
|
|
60
|
+
args: Args;
|
|
61
|
+
ctx: McpToolContext;
|
|
62
|
+
}) => Promise<unknown> | unknown;
|
|
58
63
|
}
|
|
59
64
|
/**
|
|
60
65
|
* Wraps a successful tool result as an MCP `CallToolResult`.
|
|
@@ -69,15 +74,26 @@ export interface McpToolDef<Args extends Record<string, unknown> = Record<string
|
|
|
69
74
|
* @complexity O(n) in the number of content-array entries, only when `payload` already looks like an
|
|
70
75
|
* envelope; O(1) otherwise.
|
|
71
76
|
*/
|
|
72
|
-
export declare function okResult(payload
|
|
77
|
+
export declare function okResult({ payload }: {
|
|
78
|
+
payload: unknown;
|
|
79
|
+
}): CallToolResult;
|
|
73
80
|
/** Wraps a tool failure as an MCP `isError` result. */
|
|
74
|
-
export declare function errorResult(message
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
export declare function errorResult({ message }: {
|
|
82
|
+
message: string;
|
|
83
|
+
}): CallToolResult;
|
|
84
|
+
/** Returns a validated non-empty string, or throws the existing caller-facing error. */
|
|
85
|
+
export declare function requireString({ value, name }: {
|
|
86
|
+
value: unknown;
|
|
87
|
+
name: string;
|
|
88
|
+
}): string;
|
|
77
89
|
/** Projects a tool list into the `Tool[]` shape `tools/list` returns. */
|
|
78
|
-
export declare function toolsToList(tools
|
|
90
|
+
export declare function toolsToList({ tools }: {
|
|
91
|
+
tools: readonly McpToolDef[];
|
|
92
|
+
}): Tool[];
|
|
79
93
|
/** Builds a name -> def lookup, throwing if two tools in `tools` share a name (a caller-configuration bug, surfaced eagerly at server-construction time rather than silently letting the second registration shadow the first). */
|
|
80
|
-
export declare function buildToolIndex(tools
|
|
94
|
+
export declare function buildToolIndex({ tools }: {
|
|
95
|
+
tools: readonly McpToolDef[];
|
|
96
|
+
}): Map<string, McpToolDef>;
|
|
81
97
|
/**
|
|
82
98
|
* `tools/call` dispatch: looks up `name` in `tools`, validates `rawArgs` against the tool's
|
|
83
99
|
* declared `inputSchema`, runs its handler, and converts the outcome to a `CallToolResult` — an
|
|
@@ -90,5 +106,11 @@ export declare function buildToolIndex(tools: readonly McpToolDef[]): Map<string
|
|
|
90
106
|
* result: either can end up echoing caller- or daemon-supplied text, and it is cheaper to sanitize
|
|
91
107
|
* unconditionally than to prove which call site needs it.
|
|
92
108
|
*/
|
|
93
|
-
export declare function handleToolCall(name
|
|
109
|
+
export declare function handleToolCall({ name, tools, ctx }: {
|
|
110
|
+
name: string;
|
|
111
|
+
tools: ReadonlyMap<string, McpToolDef>;
|
|
112
|
+
ctx: McpToolContext;
|
|
113
|
+
}, { rawArgs }?: {
|
|
114
|
+
rawArgs?: Record<string, unknown> | undefined;
|
|
115
|
+
}): Promise<CallToolResult>;
|
|
94
116
|
//# sourceMappingURL=tool-protocol.d.ts.map
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* projection, and `tools/call` dispatch (look up a tool by name, run its
|
|
7
7
|
* handler, wrap the result). None of this touches `@modelcontextprotocol/sdk`'s
|
|
8
8
|
* `Server`/transport classes or the network — a tool `handler` is just
|
|
9
|
-
* `(args, ctx) => value | Promise<value>` that either returns a
|
|
9
|
+
* `({ args, ctx }) => value | Promise<value>` that either returns a
|
|
10
10
|
* JSON-serializable payload (wrapped as a successful MCP result) or throws
|
|
11
11
|
* (wrapped as an `{isError:true}` MCP result). `./tool-server.js` is the thin
|
|
12
12
|
* layer that wires this to a real `Server` + `StdioServerTransport`.
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* `delegated-tool-bridge.ts` already keeps an image block in its returned `ToolExecutionResult.output`
|
|
23
23
|
* (it is model-safe per that package's `tool-result-surfaces.ts`), but this module still turned it
|
|
24
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 (
|
|
25
|
+
* text** in a separate, traced incident ( Decision 5): a `resource` block carrying the token
|
|
26
26
|
* fell to the same stringify path because an earlier version of this fix only recognized `text` and
|
|
27
27
|
* `image`, and `resource` was not yet in that hand-rolled allowlist.
|
|
28
28
|
*
|
|
@@ -41,9 +41,10 @@
|
|
|
41
41
|
* allowlist" into "remember to bump the SDK", not into an unconditional guarantee. It also does not
|
|
42
42
|
* (and cannot) judge intent: a handler that puts a secret inside an otherwise well-formed `text` block
|
|
43
43
|
* will still have it pass through as designed — withholding a value from the model is the handler's
|
|
44
|
-
* responsibility (
|
|
44
|
+
* responsibility ( Decision 4), not this generic wrapper's.
|
|
45
|
+
* See docs/decisions/DR-002-protocol-resource-boundary.md.
|
|
45
46
|
*/
|
|
46
|
-
import { sanitizeUntrustedText } from '@jini-ai/
|
|
47
|
+
import { sanitizeUntrustedText } from '@jini-ai/core/text';
|
|
47
48
|
import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker';
|
|
48
49
|
import { ContentBlockSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
49
50
|
/**
|
|
@@ -60,7 +61,7 @@ import { ContentBlockSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
|
60
61
|
* @complexity O(1).
|
|
61
62
|
* @overallScore 100/100
|
|
62
63
|
*/
|
|
63
|
-
export function daemonCallOptions(ctx) {
|
|
64
|
+
export function daemonCallOptions({ ctx }) {
|
|
64
65
|
return {
|
|
65
66
|
fetchImpl: ctx.fetchImpl,
|
|
66
67
|
...(ctx.authHeaders !== undefined ? { headers: { ...ctx.authHeaders } } : {}),
|
|
@@ -112,7 +113,7 @@ function isMcpContentEnvelope(payload) {
|
|
|
112
113
|
* @complexity O(n) in the number of content-array entries, only when `payload` already looks like an
|
|
113
114
|
* envelope; O(1) otherwise.
|
|
114
115
|
*/
|
|
115
|
-
export function okResult(payload) {
|
|
116
|
+
export function okResult({ payload }) {
|
|
116
117
|
if (isMcpContentEnvelope(payload)) {
|
|
117
118
|
return { content: payload.content };
|
|
118
119
|
}
|
|
@@ -123,17 +124,18 @@ export function okResult(payload) {
|
|
|
123
124
|
return { content: [{ type: 'text', text }] };
|
|
124
125
|
}
|
|
125
126
|
/** Wraps a tool failure as an MCP `isError` result. */
|
|
126
|
-
export function errorResult(message) {
|
|
127
|
+
export function errorResult({ message }) {
|
|
127
128
|
return { isError: true, content: [{ type: 'text', text: message }] };
|
|
128
129
|
}
|
|
129
|
-
/**
|
|
130
|
-
export function requireString(value, name) {
|
|
130
|
+
/** Returns a validated non-empty string, or throws the existing caller-facing error. */
|
|
131
|
+
export function requireString({ value, name }) {
|
|
131
132
|
if (typeof value !== 'string' || value.length === 0) {
|
|
132
133
|
throw new Error(`${name} is required (string).`);
|
|
133
134
|
}
|
|
135
|
+
return value;
|
|
134
136
|
}
|
|
135
137
|
/** Projects a tool list into the `Tool[]` shape `tools/list` returns. */
|
|
136
|
-
export function toolsToList(tools) {
|
|
138
|
+
export function toolsToList({ tools }) {
|
|
137
139
|
return tools.map((tool) => ({
|
|
138
140
|
name: tool.name,
|
|
139
141
|
description: tool.description,
|
|
@@ -142,7 +144,7 @@ export function toolsToList(tools) {
|
|
|
142
144
|
}));
|
|
143
145
|
}
|
|
144
146
|
/** Builds a name -> def lookup, throwing if two tools in `tools` share a name (a caller-configuration bug, surfaced eagerly at server-construction time rather than silently letting the second registration shadow the first). */
|
|
145
|
-
export function buildToolIndex(tools) {
|
|
147
|
+
export function buildToolIndex({ tools }) {
|
|
146
148
|
const index = new Map();
|
|
147
149
|
for (const tool of tools) {
|
|
148
150
|
if (index.has(tool.name)) {
|
|
@@ -187,10 +189,10 @@ function validatorForTool(tool) {
|
|
|
187
189
|
* result: either can end up echoing caller- or daemon-supplied text, and it is cheaper to sanitize
|
|
188
190
|
* unconditionally than to prove which call site needs it.
|
|
189
191
|
*/
|
|
190
|
-
export async function handleToolCall(name,
|
|
192
|
+
export async function handleToolCall({ name, tools, ctx }, { rawArgs } = {}) {
|
|
191
193
|
const tool = tools.get(name);
|
|
192
194
|
if (tool === undefined) {
|
|
193
|
-
return errorResult(`unknown tool: ${name}`);
|
|
195
|
+
return errorResult({ message: `unknown tool: ${name}` });
|
|
194
196
|
}
|
|
195
197
|
const args = rawArgs ?? {};
|
|
196
198
|
// Validation gets its own error boundary, separate from the handler's below. The
|
|
@@ -205,21 +207,21 @@ export async function handleToolCall(name, rawArgs, tools, ctx) {
|
|
|
205
207
|
try {
|
|
206
208
|
const validation = validatorForTool(tool)(args);
|
|
207
209
|
if (!validation.valid) {
|
|
208
|
-
return errorResult(sanitizeUntrustedText(`invalid arguments for ${name}: ${validation.errorMessage}`));
|
|
210
|
+
return errorResult({ message: sanitizeUntrustedText({ text: `invalid arguments for ${name}: ${validation.errorMessage}` }) });
|
|
209
211
|
}
|
|
210
212
|
validatedArgs = validation.data;
|
|
211
213
|
}
|
|
212
214
|
catch (err) {
|
|
213
215
|
const message = err instanceof Error ? err.message : String(err);
|
|
214
|
-
return errorResult(sanitizeUntrustedText(`invalid arguments for ${name}: ${message}`));
|
|
216
|
+
return errorResult({ message: sanitizeUntrustedText({ text: `invalid arguments for ${name}: ${message}` }) });
|
|
215
217
|
}
|
|
216
218
|
try {
|
|
217
|
-
const result = await tool.handler(validatedArgs, ctx);
|
|
218
|
-
return okResult(result);
|
|
219
|
+
const result = await tool.handler({ args: validatedArgs, ctx: ctx });
|
|
220
|
+
return okResult({ payload: result });
|
|
219
221
|
}
|
|
220
222
|
catch (err) {
|
|
221
223
|
const message = err instanceof Error ? err.message : String(err);
|
|
222
|
-
return errorResult(sanitizeUntrustedText(message));
|
|
224
|
+
return errorResult({ message: sanitizeUntrustedText({ text: message }) });
|
|
223
225
|
}
|
|
224
226
|
}
|
|
225
227
|
//# sourceMappingURL=tool-protocol.js.map
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* project/skill/plugin `instructions` prose — replaced by a caller-supplied,
|
|
10
10
|
* bounded, explicit `tools: readonly McpToolDef[]` list. A caller is the
|
|
11
11
|
* *first* user of this mechanism, not the only one it will ever support (see
|
|
12
|
-
* `
|
|
12
|
+
* `archived provenance ledger`'s 2026-07-21 addition for the full design-decision note).
|
|
13
13
|
*
|
|
14
14
|
* `tools` capability is always advertised (`capabilities: {tools: {}}`).
|
|
15
15
|
* `resources` capability (`ListResourcesRequestSchema`/
|
|
@@ -18,61 +18,72 @@
|
|
|
18
18
|
* optional `resources` option — a 2026-07-21 addition once this package
|
|
19
19
|
* shipped its first genuinely portable resource (`../resources/
|
|
20
20
|
* active-resource.js`'s `jini://active`, see that file's module doc and
|
|
21
|
-
* `
|
|
21
|
+
* `archived provenance ledger`'s 2026-07-21 addition for why the *rest* of OD's resource
|
|
22
22
|
* surface — `od://skills/...`, `od://design-systems/...` — still has no
|
|
23
23
|
* kernel equivalent and stays unported). A caller passing no `resources`
|
|
24
24
|
* gets the exact same `capabilities: {tools: {}}`-only server this module
|
|
25
25
|
* always produced.
|
|
26
26
|
*/
|
|
27
27
|
import type { Readable, Writable } from 'node:stream';
|
|
28
|
+
import type { McpTransportLike } from './ports.js';
|
|
29
|
+
export type { McpTransportLike } from './ports.js';
|
|
28
30
|
import type { ServerOptions } from '@modelcontextprotocol/sdk/server/index.js';
|
|
29
31
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
30
32
|
import type { CallToolResult, Implementation, ListResourcesResult, ListToolsResult, ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';
|
|
31
33
|
import { type McpResourceDef } from './resource-protocol.js';
|
|
32
34
|
import { type McpToolDef } from './tool-protocol.js';
|
|
33
|
-
/**
|
|
34
|
-
* The minimal surface this module needs from an MCP `Server` instance. A real
|
|
35
|
-
* `@modelcontextprotocol/sdk` `Server` satisfies this structurally (its own `setRequestHandler`
|
|
36
|
-
* is more general — one generic method covering every request schema — and TS accepts the
|
|
37
|
-
* narrowing); a test can substitute a lightweight fake without spinning up a real transport.
|
|
38
|
-
* Method-shorthand syntax (not arrow-typed properties) is deliberate: it keeps parameter checking
|
|
39
|
-
* bivariant, so a handler typed against a *specific* request shape (as the two internal handlers
|
|
40
|
-
* below are) is still assignable here even under `strict`.
|
|
41
|
-
*/
|
|
35
|
+
/** Object-shaped server port. The default adapter translates it to the SDK interface. */
|
|
42
36
|
export interface McpServerLike {
|
|
43
|
-
setRequestHandler(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
37
|
+
setRequestHandler(args: {
|
|
38
|
+
schema: typeof ListToolsRequestSchema;
|
|
39
|
+
handler: () => Promise<ListToolsResult>;
|
|
40
|
+
}): void;
|
|
41
|
+
setRequestHandler(args: {
|
|
42
|
+
schema: typeof CallToolRequestSchema;
|
|
43
|
+
handler: (requiredArgs: {
|
|
44
|
+
request: {
|
|
45
|
+
params: {
|
|
46
|
+
name: string;
|
|
47
|
+
arguments?: Record<string, unknown>;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
}, optionalArgs?: {
|
|
51
|
+
extra?: {
|
|
52
|
+
signal?: AbortSignal;
|
|
53
|
+
};
|
|
54
|
+
}) => Promise<CallToolResult>;
|
|
55
|
+
}): void;
|
|
56
|
+
setRequestHandler(args: {
|
|
57
|
+
schema: typeof ListResourcesRequestSchema;
|
|
58
|
+
handler: () => Promise<ListResourcesResult>;
|
|
59
|
+
}): void;
|
|
60
|
+
setRequestHandler(args: {
|
|
61
|
+
schema: typeof ReadResourceRequestSchema;
|
|
62
|
+
handler: (requiredArgs: {
|
|
63
|
+
request: {
|
|
64
|
+
params: {
|
|
65
|
+
uri: string;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
}) => Promise<ReadResourceResult>;
|
|
69
|
+
}): void;
|
|
70
|
+
connect(args: {
|
|
71
|
+
transport: McpTransportLike;
|
|
72
|
+
}): Promise<void>;
|
|
59
73
|
}
|
|
60
|
-
/**
|
|
61
|
-
export interface
|
|
62
|
-
|
|
63
|
-
onclose?: (() => void) | undefined;
|
|
64
|
-
close(): Promise<void>;
|
|
65
|
-
}
|
|
66
|
-
export interface McpToolServerOptions {
|
|
67
|
-
/** Advertised to the MCP client during initialization. */
|
|
74
|
+
/** Required identity, tool surface, and daemon URL resolver. */
|
|
75
|
+
export interface McpToolServerRequiredArgs {
|
|
76
|
+
/** Identity advertised to the MCP client during initialization. */
|
|
68
77
|
readonly name: string;
|
|
69
78
|
readonly version: string;
|
|
70
|
-
/**
|
|
79
|
+
/** Duplicate tool names throw at construction. */
|
|
71
80
|
readonly tools: readonly McpToolDef[];
|
|
81
|
+
/** Resolves once when the returned handle runs. */
|
|
82
|
+
readonly resolveBaseUrl: () => Promise<string> | string;
|
|
83
|
+
}
|
|
84
|
+
export interface McpToolServerOptions {
|
|
72
85
|
/** 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. */
|
|
73
86
|
readonly resources?: readonly McpResourceDef[];
|
|
74
|
-
/** Resolves the daemon HTTP base URL once, at the start of {@link McpToolServerHandle.run}. May be sync or async (e.g. wraps `@jini-ai/cli`'s `resolveDaemonUrl`). */
|
|
75
|
-
readonly resolveBaseUrl: () => Promise<string> | string;
|
|
76
87
|
/** 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`. */
|
|
77
88
|
readonly instructions?: string;
|
|
78
89
|
/** Idle-exit window in ms. Defaults to {@link DEFAULT_IDLE_MS}. */
|
|
@@ -90,9 +101,15 @@ export interface McpToolServerOptions {
|
|
|
90
101
|
/** Defaults to `process.stdout`; inject for tests. */
|
|
91
102
|
readonly stdout?: Writable;
|
|
92
103
|
/** Test/embedding seam: builds the underlying `Server`. Defaults to the real `@modelcontextprotocol/sdk` `Server`. */
|
|
93
|
-
readonly createServer?: (
|
|
104
|
+
readonly createServer?: (args: {
|
|
105
|
+
info: Implementation;
|
|
106
|
+
options: ServerOptions;
|
|
107
|
+
}) => McpServerLike;
|
|
94
108
|
/** Test/embedding seam: builds the underlying transport. Defaults to the real `StdioServerTransport`. */
|
|
95
|
-
readonly createTransport?: (
|
|
109
|
+
readonly createTransport?: (requiredArgs: Record<string, never>, optionalArgs?: {
|
|
110
|
+
stdin?: Readable;
|
|
111
|
+
stdout?: Writable;
|
|
112
|
+
}) => McpTransportLike;
|
|
96
113
|
}
|
|
97
114
|
export interface McpToolServerHandle {
|
|
98
115
|
/**
|
|
@@ -100,14 +117,14 @@ export interface McpToolServerHandle {
|
|
|
100
117
|
* open until the client disconnects (stdin EOF) or the idle-exit window elapses. Resolves once
|
|
101
118
|
* the transport has closed.
|
|
102
119
|
*/
|
|
103
|
-
run(): Promise<void>;
|
|
120
|
+
run(required: Record<string, never>): Promise<void>;
|
|
104
121
|
}
|
|
105
122
|
/**
|
|
106
|
-
* Builds a stdio MCP server hosting `
|
|
123
|
+
* Builds a stdio MCP server hosting `requiredArgs.tools` (and, optionally, `options.resources`).
|
|
107
124
|
* Construction is synchronous and cheap (validates tool-name uniqueness via
|
|
108
125
|
* {@link buildToolIndex} and resource-uri uniqueness via {@link buildResourceIndex}); all I/O —
|
|
109
126
|
* resolving the daemon URL, connecting the transport, serving requests — happens in the returned
|
|
110
127
|
* handle's `run()`.
|
|
111
128
|
*/
|
|
112
|
-
export declare function createMcpToolServer(
|
|
129
|
+
export declare function createMcpToolServer(requiredArgs: McpToolServerRequiredArgs, options?: McpToolServerOptions): McpToolServerHandle;
|
|
113
130
|
//# sourceMappingURL=tool-server.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { adaptSdkTransport, resolveSdkTransport } from './sdk-transport.js';
|
|
1
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
4
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
@@ -6,28 +7,33 @@ import { buildResourceIndex, handleResourceRead, resourcesToList } from './resou
|
|
|
6
7
|
import { buildToolIndex, handleToolCall, toolsToList } from './tool-protocol.js';
|
|
7
8
|
/** Auto-exit an idle server after this long with no tool activity — same ceiling the OD origin used for its own stdio MCP server. */
|
|
8
9
|
const DEFAULT_IDLE_MS = 30 * 60 * 1000;
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
// SDK objects remain behind these adapters; hosts implement the object-shaped ports above.
|
|
11
|
+
function defaultCreateServer({ info, options }) {
|
|
12
|
+
const server = new Server(info, options);
|
|
13
|
+
return {
|
|
14
|
+
setRequestHandler: (({ schema, handler }) => server.setRequestHandler(schema, (request, extra) => handler({ request }, { extra }))),
|
|
15
|
+
connect: ({ transport }) => server.connect(resolveSdkTransport({ transport })),
|
|
16
|
+
};
|
|
11
17
|
}
|
|
12
|
-
function defaultCreateTransport(stdin, stdout) {
|
|
13
|
-
return new StdioServerTransport(stdin, stdout);
|
|
18
|
+
function defaultCreateTransport(_requiredArgs, { stdin, stdout } = {}) {
|
|
19
|
+
return adaptSdkTransport({ transport: new StdioServerTransport(stdin, stdout) });
|
|
14
20
|
}
|
|
15
21
|
/**
|
|
16
|
-
* Builds a stdio MCP server hosting `
|
|
22
|
+
* Builds a stdio MCP server hosting `requiredArgs.tools` (and, optionally, `options.resources`).
|
|
17
23
|
* Construction is synchronous and cheap (validates tool-name uniqueness via
|
|
18
24
|
* {@link buildToolIndex} and resource-uri uniqueness via {@link buildResourceIndex}); all I/O —
|
|
19
25
|
* resolving the daemon URL, connecting the transport, serving requests — happens in the returned
|
|
20
26
|
* handle's `run()`.
|
|
21
27
|
*/
|
|
22
|
-
export function createMcpToolServer(options) {
|
|
23
|
-
const toolIndex = buildToolIndex(
|
|
28
|
+
export function createMcpToolServer(requiredArgs, options = {}) {
|
|
29
|
+
const toolIndex = buildToolIndex({ tools: requiredArgs.tools });
|
|
24
30
|
const resources = options.resources ?? [];
|
|
25
|
-
const resourceIndex = buildResourceIndex(resources);
|
|
31
|
+
const resourceIndex = buildResourceIndex({ resources });
|
|
26
32
|
const createServer = options.createServer ?? defaultCreateServer;
|
|
27
33
|
const createTransport = options.createTransport ?? defaultCreateTransport;
|
|
28
34
|
return {
|
|
29
|
-
async run() {
|
|
30
|
-
const resolvedBaseUrl = await
|
|
35
|
+
async run(_required) {
|
|
36
|
+
const resolvedBaseUrl = await requiredArgs.resolveBaseUrl();
|
|
31
37
|
const ctx = {
|
|
32
38
|
baseUrl: String(resolvedBaseUrl).replace(/\/$/, ''),
|
|
33
39
|
fetchImpl: options.fetchImpl ?? fetch,
|
|
@@ -38,30 +44,48 @@ export function createMcpToolServer(options) {
|
|
|
38
44
|
idleMs: options.idleMs ?? DEFAULT_IDLE_MS,
|
|
39
45
|
onIdle: () => closeTransportForIdle?.(),
|
|
40
46
|
});
|
|
41
|
-
const withActivity = (handler) => (...args) => idleExit.trackRequest(() => handler(...args));
|
|
42
|
-
const server = createServer({
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
const withActivity = (handler) => (...args) => idleExit.trackRequest({ fn: () => handler(...args) });
|
|
48
|
+
const server = createServer({
|
|
49
|
+
info: { name: requiredArgs.name, version: requiredArgs.version },
|
|
50
|
+
options: {
|
|
51
|
+
capabilities: { tools: {}, ...(resources.length > 0 ? { resources: {} } : {}) },
|
|
52
|
+
...(options.instructions !== undefined ? { instructions: options.instructions } : {}),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
server.setRequestHandler({
|
|
56
|
+
schema: ListToolsRequestSchema,
|
|
57
|
+
handler: withActivity(async () => ({ tools: toolsToList({ tools: requiredArgs.tools }) })),
|
|
58
|
+
});
|
|
59
|
+
server.setRequestHandler({
|
|
60
|
+
schema: CallToolRequestSchema,
|
|
61
|
+
handler: withActivity(async ({ request }, { extra } = {}) => handleToolCall({ name: request.params.name, tools: toolIndex, ctx: extra?.signal !== undefined ? { ...ctx, signal: extra.signal } : ctx }, { rawArgs: request.params.arguments })),
|
|
45
62
|
});
|
|
46
|
-
server.setRequestHandler(ListToolsRequestSchema, withActivity(async () => ({ tools: toolsToList(options.tools) })));
|
|
47
|
-
server.setRequestHandler(CallToolRequestSchema, withActivity(async (request, extra) => handleToolCall(request.params.name, request.params.arguments, toolIndex, extra?.signal !== undefined ? { ...ctx, signal: extra.signal } : ctx)));
|
|
48
63
|
if (resources.length > 0) {
|
|
49
|
-
server.setRequestHandler(
|
|
50
|
-
|
|
64
|
+
server.setRequestHandler({
|
|
65
|
+
schema: ListResourcesRequestSchema,
|
|
66
|
+
handler: withActivity(async () => ({ resources: resourcesToList({ resources }) })),
|
|
67
|
+
});
|
|
68
|
+
server.setRequestHandler({
|
|
69
|
+
schema: ReadResourceRequestSchema,
|
|
70
|
+
handler: withActivity(async ({ request }) => handleResourceRead({ uri: request.params.uri, resources: resourceIndex, ctx })),
|
|
71
|
+
});
|
|
51
72
|
}
|
|
52
|
-
const transport = createTransport(
|
|
73
|
+
const transport = createTransport({}, {
|
|
74
|
+
...(options.stdin === undefined ? {} : { stdin: options.stdin }),
|
|
75
|
+
...(options.stdout === undefined ? {} : { stdout: options.stdout }),
|
|
76
|
+
});
|
|
53
77
|
try {
|
|
54
78
|
closeTransportForIdle = () => {
|
|
55
|
-
void transport.close().catch(() => { });
|
|
79
|
+
void transport.close({}).catch(() => { });
|
|
56
80
|
};
|
|
57
|
-
await server.connect(transport);
|
|
81
|
+
await server.connect({ transport });
|
|
58
82
|
// `connect()` sets `transport.onmessage` to its own protocol-dispatch handler; wrap it so
|
|
59
83
|
// every inbound message also counts as activity for the idle-exit timer, without losing
|
|
60
84
|
// the SDK's own routing.
|
|
61
85
|
const sdkOnMessage = transport.onmessage;
|
|
62
|
-
transport.onmessage = (message) => {
|
|
63
|
-
idleExit.noteActivity();
|
|
64
|
-
sdkOnMessage?.(message);
|
|
86
|
+
transport.onmessage = ({ message }) => {
|
|
87
|
+
idleExit.noteActivity({});
|
|
88
|
+
sdkOnMessage?.({ message });
|
|
65
89
|
};
|
|
66
90
|
const stdin = options.stdin ?? process.stdin;
|
|
67
91
|
await new Promise((resolve) => {
|
|
@@ -71,7 +95,7 @@ export function createMcpToolServer(options) {
|
|
|
71
95
|
if (finished)
|
|
72
96
|
return;
|
|
73
97
|
finished = true;
|
|
74
|
-
idleExit.dispose();
|
|
98
|
+
idleExit.dispose({});
|
|
75
99
|
resolve();
|
|
76
100
|
};
|
|
77
101
|
transport.onclose = () => {
|
|
@@ -79,7 +103,7 @@ export function createMcpToolServer(options) {
|
|
|
79
103
|
done();
|
|
80
104
|
};
|
|
81
105
|
const closeTransportForStdin = () => {
|
|
82
|
-
void transport.close().catch(() => done());
|
|
106
|
+
void transport.close({}).catch(() => done());
|
|
83
107
|
};
|
|
84
108
|
// Hold the process open until the client disconnects (stdin EOF) — `connect()` only
|
|
85
109
|
// starts the transport, it doesn't wait for the stream to close.
|
|
@@ -88,7 +112,7 @@ export function createMcpToolServer(options) {
|
|
|
88
112
|
});
|
|
89
113
|
}
|
|
90
114
|
finally {
|
|
91
|
-
idleExit.dispose();
|
|
115
|
+
idleExit.dispose({});
|
|
92
116
|
closeTransportForIdle = null;
|
|
93
117
|
}
|
|
94
118
|
},
|
|
@@ -39,12 +39,12 @@ export const searchComponentsTool = {
|
|
|
39
39
|
additionalProperties: false,
|
|
40
40
|
},
|
|
41
41
|
annotations: { ...READ_ANNOTATIONS, title: 'Search the interactive-UI component catalog' },
|
|
42
|
-
handler: async (args, ctx) => {
|
|
43
|
-
requireString(args.query, 'query');
|
|
44
|
-
const params = new URLSearchParams({ q:
|
|
42
|
+
handler: async ({ args, ctx }) => {
|
|
43
|
+
const query = requireString({ value: args.query, name: 'query' });
|
|
44
|
+
const params = new URLSearchParams({ q: query });
|
|
45
45
|
if (typeof args.limit === 'number')
|
|
46
46
|
params.set('limit', String(args.limit));
|
|
47
|
-
const data = await getDaemonJson(ctx.baseUrl, `/api/components/search?${params.toString()}
|
|
47
|
+
const data = await getDaemonJson({ baseUrl: ctx.baseUrl, route: `/api/components/search?${params.toString()}` }, daemonCallOptions({ ctx }));
|
|
48
48
|
return data.hits;
|
|
49
49
|
},
|
|
50
50
|
};
|
|
@@ -61,9 +61,9 @@ export const describeComponentTool = {
|
|
|
61
61
|
additionalProperties: false,
|
|
62
62
|
},
|
|
63
63
|
annotations: { ...READ_ANNOTATIONS, title: 'Describe an interactive-UI component' },
|
|
64
|
-
handler: async (args, ctx) => {
|
|
65
|
-
requireString(args.id, 'id');
|
|
66
|
-
return getDaemonJson(ctx.baseUrl, `/api/components/${encodeURIComponent(
|
|
64
|
+
handler: async ({ args, ctx }) => {
|
|
65
|
+
const id = requireString({ value: args.id, name: 'id' });
|
|
66
|
+
return getDaemonJson({ baseUrl: ctx.baseUrl, route: `/api/components/${encodeURIComponent(id)}` }, daemonCallOptions({ ctx }));
|
|
67
67
|
},
|
|
68
68
|
};
|
|
69
69
|
/** Both component-catalog discovery tools, ready to pass as `createMcpToolServer`'s `tools` option. */
|