@jini-ai/mcp 0.6.16 → 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 +11 -0
- package/dist/bin/serve.js +12 -81
- package/dist/core/config.js +3 -1
- package/dist/core/tokens.js +3 -1
- package/dist/federation/bootstrap.d.ts +3 -4
- package/dist/federation/bootstrap.js +11 -3
- package/dist/federation/reload.d.ts +6 -1
- package/dist/federation/reload.js +24 -5
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/server/daemon-client.d.ts +2 -2
- package/dist/server/daemon-client.js +4 -1
- package/dist/server/http-tool-server.d.ts +44 -0
- package/dist/server/http-tool-server.js +272 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +2 -0
- package/dist/server/run-tool-host.d.ts +28 -0
- package/dist/server/run-tool-host.js +103 -0
- package/dist/server/sdk-transport.d.ts +7 -1
- package/dist/server/tool-arguments.d.ts +28 -4
- package/dist/server/tool-arguments.js +47 -9
- package/dist/server/tool-protocol.js +39 -9
- package/dist/server/tool-result-pages.d.ts +21 -0
- package/dist/server/tool-result-pages.js +74 -0
- package/dist/server/tool-server.d.ts +11 -5
- package/dist/server/tool-server.js +78 -53
- package/dist/server/tools/delegated-tool.d.ts +7 -6
- package/dist/server/tools/delegated-tool.js +8 -3
- package/dist/server/tools/wait.d.ts +5 -0
- package/dist/server/tools/wait.js +17 -0
- package/package.json +7 -7
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/** SDK Streamable HTTP adapter for the existing tool host. Capabilities remain in host memory. */
|
|
2
|
+
import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
5
|
+
import { CancelledNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { adaptSdkTransport } from './sdk-transport.js';
|
|
7
|
+
import { createMcpToolServer } from './tool-server.js';
|
|
8
|
+
/** Embeddings must gate CLI HTTP on retained-session support rather than a protocol probe. */
|
|
9
|
+
export const MCP_HTTP_SESSION_MODE_SUPPORTED = true;
|
|
10
|
+
const MAX_BODY_BYTES = 1_000_000;
|
|
11
|
+
const MAX_REQUESTS = 16;
|
|
12
|
+
/** Constant-time credential comparison after a length check. O(credential bytes). */
|
|
13
|
+
function authorized({ request, token }, _options = {}) {
|
|
14
|
+
const actual = Buffer.from(request.headers.authorization ?? '');
|
|
15
|
+
const expected = Buffer.from(`Bearer ${token}`);
|
|
16
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
17
|
+
}
|
|
18
|
+
/** Read at most one bounded JSON document. The receive deadline ends before tool execution.
|
|
19
|
+
* Returns HTTP status on invalid/oversized input, without interpreting MCP messages. O(body bytes).
|
|
20
|
+
*/
|
|
21
|
+
async function readBody({ request }, _options = {}) {
|
|
22
|
+
const chunks = [];
|
|
23
|
+
let bytes = 0;
|
|
24
|
+
for await (const chunk of request.iterator({ destroyOnReturn: false })) {
|
|
25
|
+
const buffer = Buffer.from(chunk);
|
|
26
|
+
bytes += buffer.length;
|
|
27
|
+
if (bytes > MAX_BODY_BYTES)
|
|
28
|
+
return { status: 413 };
|
|
29
|
+
chunks.push(buffer);
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return { body: JSON.parse(Buffer.concat(chunks).toString('utf8')) };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { status: 400 };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Bind a private loopback listener. Desktop requests use fresh stateless SDK transports;
|
|
39
|
+
* run listeners opt into one retained SDK session and the same host-owned dispatch contract. Auth precedes body parsing; browsers are always refused.
|
|
40
|
+
* @example const host = await startMcpHttpToolServer({ name, version, tools, resolveBaseUrl });
|
|
41
|
+
* @throws Listen/configuration errors; the embedding owns degraded startup and retries.
|
|
42
|
+
* @complexity O(body bytes + dispatched handler), with 16 requests and 1 MB per body maximum.
|
|
43
|
+
*/
|
|
44
|
+
export async function startMcpHttpToolServer(required, options = {}) {
|
|
45
|
+
const host = createMcpToolServer(required, options);
|
|
46
|
+
const token = randomBytes(32).toString('hex');
|
|
47
|
+
const pending = new Set();
|
|
48
|
+
const sessionTransport = options.sessionMode
|
|
49
|
+
? new StreamableHTTPServerTransport({ sessionIdGenerator: randomUUID }) : undefined;
|
|
50
|
+
let sessionConnection;
|
|
51
|
+
let closing;
|
|
52
|
+
let stopped = false;
|
|
53
|
+
let retry;
|
|
54
|
+
const server = (options.createHttpServer ?? createServer)((request, response) => {
|
|
55
|
+
const work = handle(request, response).catch(() => {
|
|
56
|
+
options.warn?.('MCP HTTP request failed');
|
|
57
|
+
if (!response.headersSent)
|
|
58
|
+
response.writeHead(500).end();
|
|
59
|
+
else
|
|
60
|
+
response.destroy();
|
|
61
|
+
});
|
|
62
|
+
pending.add(work);
|
|
63
|
+
void work.finally(() => pending.delete(work));
|
|
64
|
+
});
|
|
65
|
+
server.headersTimeout = 5000;
|
|
66
|
+
server.requestTimeout = 15_000;
|
|
67
|
+
server.maxConnections = MAX_REQUESTS;
|
|
68
|
+
/** Gate admission before acquiring bytes or SDK state. O(1) plus a bounded body and tool work. */
|
|
69
|
+
async function handle(request, response) {
|
|
70
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
71
|
+
if (request.headers.origin !== undefined || !authorized({ request, token })) {
|
|
72
|
+
response.writeHead(403, { Connection: 'close' }).end();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (request.url !== '/mcp') {
|
|
76
|
+
response.writeHead(404).end();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const allowed = options.sessionMode ? ['POST', 'GET', 'DELETE'] : ['POST'];
|
|
80
|
+
if (!allowed.includes(request.method ?? '')) {
|
|
81
|
+
response.writeHead(405, { Allow: allowed.join(', ') }).end();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (stopped || pending.size >= MAX_REQUESTS) {
|
|
85
|
+
response.writeHead(503).end();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await dispatch(request, response);
|
|
89
|
+
}
|
|
90
|
+
/** Dispose the SDK connection after response delivery, including failed/abandoned requests.
|
|
91
|
+
* The SDK owns initialize, notifications, validation, list and call; no handwritten dispatch.
|
|
92
|
+
*/
|
|
93
|
+
async function dispatch(request, response) {
|
|
94
|
+
let body;
|
|
95
|
+
if (request.method === 'POST') {
|
|
96
|
+
// Bound slow authenticated senders too. This timer never limits tool execution.
|
|
97
|
+
const deadline = setTimeout(() => request.destroy(), 15_000);
|
|
98
|
+
deadline.unref();
|
|
99
|
+
let input;
|
|
100
|
+
try {
|
|
101
|
+
input = await readBody({ request });
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
clearTimeout(deadline);
|
|
105
|
+
}
|
|
106
|
+
if ('status' in input) {
|
|
107
|
+
response.writeHead(input.status, { Connection: 'close' }).end();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
body = input.body;
|
|
111
|
+
}
|
|
112
|
+
if (sessionTransport) {
|
|
113
|
+
sessionConnection ??= host.run({ transport: adaptSdkTransport({ transport: sessionTransport }) }).then(() => {
|
|
114
|
+
const onmessage = sessionTransport.onmessage;
|
|
115
|
+
sessionTransport.onmessage = (message, extra) => {
|
|
116
|
+
onmessage?.(message, extra);
|
|
117
|
+
// The SDK aborts the handler and suppresses its reply on cancellation. Use its
|
|
118
|
+
// cancellable SSE response stream so the original POST can also finish draining.
|
|
119
|
+
const cancellation = CancelledNotificationSchema.safeParse(message);
|
|
120
|
+
if (cancellation.success && cancellation.data.params.requestId !== undefined) {
|
|
121
|
+
sessionTransport.closeSSEStream(cancellation.data.params.requestId);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
await sessionConnection;
|
|
126
|
+
if (request.method === 'GET' && stopped) {
|
|
127
|
+
response.writeHead(503).end();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await sessionTransport.handleRequest(request, response, body);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// No sessionIdGenerator means stateless mode in the pinned SDK.
|
|
134
|
+
const transport = new StreamableHTTPServerTransport({ enableJsonResponse: true });
|
|
135
|
+
try {
|
|
136
|
+
await host.run({ transport: adaptSdkTransport({ transport }) });
|
|
137
|
+
await transport.handleRequest(request, response, body);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await transport.close();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
await new Promise((resolve, reject) => {
|
|
144
|
+
server.once('error', reject);
|
|
145
|
+
server.listen(0, '127.0.0.1', () => { server.removeListener('error', reject); resolve(); });
|
|
146
|
+
});
|
|
147
|
+
const address = server.address();
|
|
148
|
+
if (!address || typeof address === 'string')
|
|
149
|
+
throw new Error('MCP HTTP listener has no TCP address');
|
|
150
|
+
const port = address.port;
|
|
151
|
+
/** Repair only the listener, retaining the exact URL/token and never replaying tool work. O(1). */
|
|
152
|
+
function repairListener() {
|
|
153
|
+
if (stopped || server.listening || retry)
|
|
154
|
+
return;
|
|
155
|
+
options.warn?.('MCP HTTP listener unavailable; retrying automatically');
|
|
156
|
+
retry = setTimeout(() => {
|
|
157
|
+
retry = undefined;
|
|
158
|
+
if (!stopped && !server.listening)
|
|
159
|
+
server.listen(port, '127.0.0.1');
|
|
160
|
+
}, 1000);
|
|
161
|
+
retry.unref();
|
|
162
|
+
}
|
|
163
|
+
server.on('error', repairListener);
|
|
164
|
+
server.on('close', repairListener);
|
|
165
|
+
return {
|
|
166
|
+
url: `http://127.0.0.1:${port}/mcp`, token,
|
|
167
|
+
close(_required, _optional = {}) {
|
|
168
|
+
stopped = true;
|
|
169
|
+
clearTimeout(retry);
|
|
170
|
+
closing ??= (async () => {
|
|
171
|
+
// End passive SSE first; otherwise server.close() would wait for the client forever.
|
|
172
|
+
// Active POST streams retain accepted work until completion or client cancellation.
|
|
173
|
+
sessionTransport?.closeStandaloneSSEStream();
|
|
174
|
+
const listenerClosed = new Promise((resolve, reject) => {
|
|
175
|
+
server.close(error => error && error.code !== 'ERR_SERVER_NOT_RUNNING' ? reject(error) : resolve());
|
|
176
|
+
});
|
|
177
|
+
await Promise.all(pending);
|
|
178
|
+
await sessionConnection?.catch(() => { });
|
|
179
|
+
await sessionTransport?.close();
|
|
180
|
+
server.closeIdleConnections();
|
|
181
|
+
await listenerClosed;
|
|
182
|
+
})();
|
|
183
|
+
return closing;
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** Own one shared HTTP listener independently of site/window lifecycles. No I/O until capability()
|
|
188
|
+
* is called. Concurrent callers share a start; shutdown cancels retries and drains a pending bind.
|
|
189
|
+
* @example const runtime = createMcpHttpToolServerRuntime({ name, version, tools, resolveBaseUrl });
|
|
190
|
+
* @complexity O(1) state beyond the bounded HTTP listener and caller-supplied tool table.
|
|
191
|
+
*/
|
|
192
|
+
export function createMcpHttpToolServerRuntime(required, { start = startMcpHttpToolServer, ...options } = {}) {
|
|
193
|
+
let host;
|
|
194
|
+
let starting;
|
|
195
|
+
let retry;
|
|
196
|
+
let stopped = false;
|
|
197
|
+
let closing;
|
|
198
|
+
const listeners = new Set();
|
|
199
|
+
const subscriptions = new Set();
|
|
200
|
+
/** Single-flight bind; errors expose no capability or native error text. O(1) beyond bind. */
|
|
201
|
+
function ensureListener() {
|
|
202
|
+
if (stopped)
|
|
203
|
+
return Promise.resolve(undefined);
|
|
204
|
+
if (host)
|
|
205
|
+
return Promise.resolve(host);
|
|
206
|
+
if (starting)
|
|
207
|
+
return starting;
|
|
208
|
+
starting = start(required, options).then(async (handle) => {
|
|
209
|
+
if (stopped) {
|
|
210
|
+
await handle.close({});
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
host = handle;
|
|
214
|
+
for (const listener of listeners)
|
|
215
|
+
listener({ url: handle.url, token: handle.token });
|
|
216
|
+
return handle;
|
|
217
|
+
}).catch(() => {
|
|
218
|
+
if (!stopped) {
|
|
219
|
+
options.warn?.('MCP HTTP listener unavailable; retrying automatically');
|
|
220
|
+
retry ??= setTimeout(() => { retry = undefined; void ensureListener(); }, 1000);
|
|
221
|
+
retry.unref();
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}).finally(() => { starting = undefined; });
|
|
225
|
+
return starting;
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
subscribe({ listener }, _optional = {}) {
|
|
229
|
+
if (stopped)
|
|
230
|
+
return () => { };
|
|
231
|
+
let cancelled = false;
|
|
232
|
+
let deliveryRetry;
|
|
233
|
+
const unsubscribe = () => {
|
|
234
|
+
cancelled = true;
|
|
235
|
+
clearTimeout(deliveryRetry);
|
|
236
|
+
listeners.delete(deliver);
|
|
237
|
+
subscriptions.delete(unsubscribe);
|
|
238
|
+
};
|
|
239
|
+
/** Retry only capability delivery. A subscriber's error never changes the bound host. O(1). */
|
|
240
|
+
function deliver(capability) {
|
|
241
|
+
void Promise.resolve().then(() => { if (!cancelled)
|
|
242
|
+
return listener(capability); }).catch(() => {
|
|
243
|
+
if (cancelled)
|
|
244
|
+
return;
|
|
245
|
+
options.warn?.('MCP HTTP capability delivery unavailable; retrying automatically');
|
|
246
|
+
deliveryRetry = setTimeout(() => deliver(capability), 1000);
|
|
247
|
+
deliveryRetry.unref();
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
listeners.add(deliver);
|
|
251
|
+
subscriptions.add(unsubscribe);
|
|
252
|
+
if (host)
|
|
253
|
+
deliver({ url: host.url, token: host.token });
|
|
254
|
+
void ensureListener();
|
|
255
|
+
return unsubscribe;
|
|
256
|
+
},
|
|
257
|
+
async capability(_required, _optional = {}) {
|
|
258
|
+
const handle = await ensureListener();
|
|
259
|
+
return handle ? { url: handle.url, token: handle.token } : undefined;
|
|
260
|
+
},
|
|
261
|
+
close(_required, _optional = {}) {
|
|
262
|
+
stopped = true;
|
|
263
|
+
for (const unsubscribe of subscriptions)
|
|
264
|
+
unsubscribe();
|
|
265
|
+
listeners.clear();
|
|
266
|
+
clearTimeout(retry);
|
|
267
|
+
closing ??= (async () => { await starting; await host?.close({}); host = undefined; })();
|
|
268
|
+
return closing;
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=http-tool-server.js.map
|
package/dist/server/index.d.ts
CHANGED
|
@@ -20,4 +20,6 @@ export * from './tools/tool-catalog-tools.js';
|
|
|
20
20
|
export * from './tools/component-catalog-tools.js';
|
|
21
21
|
export * from './resources/active-resource.js';
|
|
22
22
|
export * from './resources/site-guide-resource.js';
|
|
23
|
+
export * from './http-tool-server.js';
|
|
24
|
+
export * from './run-tool-host.js';
|
|
23
25
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/server/index.js
CHANGED
|
@@ -20,4 +20,6 @@ export * from './tools/tool-catalog-tools.js';
|
|
|
20
20
|
export * from './tools/component-catalog-tools.js';
|
|
21
21
|
export * from './resources/active-resource.js';
|
|
22
22
|
export * from './resources/site-guide-resource.js';
|
|
23
|
+
export * from './http-tool-server.js';
|
|
24
|
+
export * from './run-tool-host.js';
|
|
23
25
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { McpToolServerRequiredArgs, McpToolServerOptions } from './tool-server.js';
|
|
2
|
+
export interface RunToolHostInput {
|
|
3
|
+
readonly runId: string;
|
|
4
|
+
readonly resolveBaseUrl: () => Promise<string> | string;
|
|
5
|
+
}
|
|
6
|
+
export interface RunToolHostOptions {
|
|
7
|
+
readonly credential?: string;
|
|
8
|
+
readonly startupToolIds?: readonly string[];
|
|
9
|
+
readonly diagnosticToolIds?: readonly string[];
|
|
10
|
+
readonly messageRelayNonce?: string;
|
|
11
|
+
readonly delegatedToolTimeoutMs?: number;
|
|
12
|
+
readonly delegatedResultMarkers?: boolean;
|
|
13
|
+
readonly generateToolUseId?: (required: Record<string, never>) => string;
|
|
14
|
+
readonly fetchImpl?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
export interface RunToolHost {
|
|
17
|
+
readonly required: McpToolServerRequiredArgs;
|
|
18
|
+
readonly options: McpToolServerOptions;
|
|
19
|
+
}
|
|
20
|
+
/** Assemble one run's authenticated gateways, publication, resources, paging and relay.
|
|
21
|
+
* @param input - Trusted run identity and explicit daemon URL resolver.
|
|
22
|
+
* @param options - Explicit callback credential, launch metadata and optional I/O seams.
|
|
23
|
+
* @returns Transport-neutral arguments for the existing MCP host.
|
|
24
|
+
* @throws Invalid relay nonce or existing authenticated descriptor resolution errors.
|
|
25
|
+
* @complexity O(n + s) time/space and n sequential bounded GETs for n unique startup IDs and s schema bytes.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assembleRunToolHost(input: RunToolHostInput, options?: RunToolHostOptions): Promise<RunToolHost>;
|
|
28
|
+
//# sourceMappingURL=run-tool-host.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** The run MCP surface shared by stdio children and in-process HTTP listeners. No process defaults. */
|
|
2
|
+
import { createCheckMessagesTool, messageRelayInstructions } from './message-relay.js';
|
|
3
|
+
import { waitTool } from './tools/wait.js';
|
|
4
|
+
import { RUN_TOOLS } from './tools/run-tools.js';
|
|
5
|
+
import { describeToolTool, TOOL_CATALOG_TOOLS } from './tools/tool-catalog-tools.js';
|
|
6
|
+
import { COMPONENT_CATALOG_TOOLS } from './tools/component-catalog-tools.js';
|
|
7
|
+
import { createExecuteDelegatedToolTool, createExecuteReadonlyDelegatedToolTool } from './tools/delegated-tool.js';
|
|
8
|
+
import { KERNEL_RESOURCES } from './resources/active-resource.js';
|
|
9
|
+
import { createSiteGuideResource } from './resources/site-guide-resource.js';
|
|
10
|
+
const SERVER_NAME = 'jini-mcp';
|
|
11
|
+
const SERVER_VERSION = '0.0.0';
|
|
12
|
+
const INSTRUCTIONS = 'Tools proxying this host\'s Jini daemon over loopback HTTP, scoped to the run that spawned this MCP server process. ' +
|
|
13
|
+
'Use execute_delegated_tool to invoke a Jini-registered tool (never an agent-vendor-specific tool name) against that run — ' +
|
|
14
|
+
'Registered operation calls use the daemon\'s deny-by-default ToolExecutor gate; ' +
|
|
15
|
+
'MCP-local names use their existing validated handlers. ' +
|
|
16
|
+
'Concrete operation tools accept their own arguments directly. describe_tool publishes a concrete definition; ' +
|
|
17
|
+
'refresh tools/list when supported, or use the delegated gateways as the compatibility fallback. ' +
|
|
18
|
+
'When the host provides site_guide, consult jini://site-guide or call site_guide with operation overview ' +
|
|
19
|
+
'through execute_readonly_delegated_tool; follow with explain for the relevant topic ids. ' +
|
|
20
|
+
'If the host does not provide site_guide, report that availability gap. ' +
|
|
21
|
+
'Use host tools first. For saved oversized tool results, call read_tool_result with the resultId and follow nextOffset. ' +
|
|
22
|
+
'For attachments, use the host attachment/image tool first or a native Read/image tool on the supplied path. ' +
|
|
23
|
+
'Never use Bash, shell commands or scripts to inspect attachments or saved tool results. ' +
|
|
24
|
+
'Between background job polls, honor poll_after_seconds with wait_for({seconds: <interval up to 60>}); repeat for longer intervals. ' +
|
|
25
|
+
'Never use Bash or shell sleep between polls. A provider wait tool is optional: describe its live schema before calling it.';
|
|
26
|
+
/** Assemble one run's authenticated gateways, publication, resources, paging and relay.
|
|
27
|
+
* @param input - Trusted run identity and explicit daemon URL resolver.
|
|
28
|
+
* @param options - Explicit callback credential, launch metadata and optional I/O seams.
|
|
29
|
+
* @returns Transport-neutral arguments for the existing MCP host.
|
|
30
|
+
* @throws Invalid relay nonce or existing authenticated descriptor resolution errors.
|
|
31
|
+
* @complexity O(n + s) time/space and n sequential bounded GETs for n unique startup IDs and s schema bytes.
|
|
32
|
+
*/
|
|
33
|
+
export async function assembleRunToolHost(input, options = {}) {
|
|
34
|
+
const { runId } = input;
|
|
35
|
+
let resolved;
|
|
36
|
+
// Startup discovery and subsequent execution must use the same resolved daemon.
|
|
37
|
+
const resolveBaseUrl = () => resolved ??= Promise.resolve().then(input.resolveBaseUrl);
|
|
38
|
+
const delegatedGatewayOptions = definedOnly({ generateToolUseId: options.generateToolUseId,
|
|
39
|
+
delegatedToolTimeoutMs: options.delegatedToolTimeoutMs, delegatedResultMarkers: options.delegatedResultMarkers });
|
|
40
|
+
const messageRelay = relayForRun({ runId, nonce: options.messageRelayNonce });
|
|
41
|
+
const required = { name: SERVER_NAME, version: SERVER_VERSION, resolveBaseUrl,
|
|
42
|
+
tools: [...buildTools({ runId, delegatedGatewayOptions }), ...(messageRelay ? [createCheckMessagesTool(messageRelay)] : [])] };
|
|
43
|
+
// Absent credential => no authHeaders, preserving unauthenticated loopback hosts.
|
|
44
|
+
const serverOptions = {
|
|
45
|
+
resources: [...KERNEL_RESOURCES, createSiteGuideResource({ runId }, delegatedGatewayOptions)],
|
|
46
|
+
instructions: INSTRUCTIONS, resultPaging: true,
|
|
47
|
+
...definedOnly({ authHeaders: options.credential ? { Authorization: `Bearer ${options.credential}` } : undefined,
|
|
48
|
+
fetchImpl: options.fetchImpl, diagnosticToolIds: options.diagnosticToolIds }),
|
|
49
|
+
...(messageRelay ? { messageRelay, instructions: `${INSTRUCTIONS} ${messageRelayInstructions(messageRelay)}` } : {}),
|
|
50
|
+
};
|
|
51
|
+
const initialTools = await resolveStartupTools({ toolIds: options.startupToolIds ?? [], resolveBaseUrl, serverOptions });
|
|
52
|
+
return { required, options: { ...serverOptions, delegatedTools: { runId, ...delegatedGatewayOptions, initialTools } } };
|
|
53
|
+
}
|
|
54
|
+
/** A malformed trusted relay flag must fail before a client receives a partially enabled surface. */
|
|
55
|
+
function relayForRun({ runId, nonce }) {
|
|
56
|
+
if (nonce === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
if (!/^[a-f0-9]{48}$/u.test(nonce))
|
|
59
|
+
throw new Error('relay requires a 48-hex-character nonce');
|
|
60
|
+
return { runId, nonce };
|
|
61
|
+
}
|
|
62
|
+
// Drops `undefined` values so an absent dependency leaves no key at all — required under
|
|
63
|
+
// `exactOptionalPropertyTypes`, and it keeps built option objects byte-identical to the
|
|
64
|
+
// pre-injection shape when nothing is overridden.
|
|
65
|
+
function definedOnly(entries) {
|
|
66
|
+
return Object.fromEntries(Object.entries(entries).filter(([, value]) => value !== undefined));
|
|
67
|
+
}
|
|
68
|
+
function buildTools({ runId, delegatedGatewayOptions }) {
|
|
69
|
+
return [
|
|
70
|
+
...RUN_TOOLS,
|
|
71
|
+
...TOOL_CATALOG_TOOLS,
|
|
72
|
+
...COMPONENT_CATALOG_TOOLS,
|
|
73
|
+
createExecuteDelegatedToolTool({ runId }, delegatedGatewayOptions),
|
|
74
|
+
// Hosted ALONGSIDE the unconstrained gateway, never instead of it. A client that gates on
|
|
75
|
+
// `readOnlyHint: true` can now reach the read half of the catalog at all; every other client
|
|
76
|
+
// sees both and picks by what its call actually does.
|
|
77
|
+
createExecuteReadonlyDelegatedToolTool({ runId }, delegatedGatewayOptions),
|
|
78
|
+
waitTool,
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Resolve each unique startup ID through the existing authenticated describe/publication owner.
|
|
83
|
+
* The temporary descriptor list seeds M1's bounded exposure; it is not an execution catalog.
|
|
84
|
+
* Failure aborts startup before any partially resolved list can reach the client.
|
|
85
|
+
* @returns Authoritative descriptors; no daemon I/O when startup IDs are absent.
|
|
86
|
+
* @throws The existing descriptor/daemon errors, reported by serve's stderr/exit boundary.
|
|
87
|
+
* @complexity O(n + s) space/time plus n sequential bounded GETs for n unique IDs and s schema bytes.
|
|
88
|
+
*/
|
|
89
|
+
async function resolveStartupTools({ toolIds, resolveBaseUrl, serverOptions }) {
|
|
90
|
+
if (toolIds.length === 0)
|
|
91
|
+
return [];
|
|
92
|
+
const descriptors = [];
|
|
93
|
+
const ctx = {
|
|
94
|
+
baseUrl: await resolveBaseUrl(),
|
|
95
|
+
fetchImpl: serverOptions.fetchImpl ?? fetch,
|
|
96
|
+
...definedOnly({ authHeaders: serverOptions.authHeaders, diagnosticToolIds: serverOptions.diagnosticToolIds }),
|
|
97
|
+
publishTool: async ({ descriptor }) => { descriptors.push(descriptor); },
|
|
98
|
+
};
|
|
99
|
+
for (const id of new Set(toolIds))
|
|
100
|
+
await describeToolTool.handler({ args: { id }, ctx });
|
|
101
|
+
return descriptors;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=run-tool-host.js.map
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
2
2
|
import type { McpTransportLike } from './ports.js';
|
|
3
|
+
type SdkTransportInput = Pick<Transport, 'start' | 'send' | 'close'> & {
|
|
4
|
+
onmessage?: Transport['onmessage'];
|
|
5
|
+
onclose?: Transport['onclose'];
|
|
6
|
+
onerror?: Transport['onerror'];
|
|
7
|
+
};
|
|
3
8
|
/** Internal boundary adapter; SDK callbacks keep their native signature behind the port. */
|
|
4
9
|
export declare function adaptSdkTransport({ transport: sdk }: {
|
|
5
|
-
transport:
|
|
10
|
+
transport: SdkTransportInput;
|
|
6
11
|
}): McpTransportLike;
|
|
7
12
|
/** Resolves adapters to the underlying SDK transport; host ports stay structurally usable. */
|
|
8
13
|
export declare function resolveSdkTransport({ transport }: {
|
|
9
14
|
transport: McpTransportLike;
|
|
10
15
|
}): Transport;
|
|
16
|
+
export {};
|
|
11
17
|
//# sourceMappingURL=sdk-transport.d.ts.map
|
|
@@ -17,12 +17,26 @@ export interface McpToolArgumentContract {
|
|
|
17
17
|
readonly argumentNormalization?: {
|
|
18
18
|
readonly aliases?: Readonly<Record<string, string>>;
|
|
19
19
|
readonly foldIntoInput?: true;
|
|
20
|
+
/** Concrete registry tools reuse the core schema normalizer before MCP validation. */
|
|
21
|
+
readonly schemaInput?: true;
|
|
20
22
|
};
|
|
21
23
|
}
|
|
24
|
+
/** Resolve this host's CLI decoration, preserving exact registered names and other servers.
|
|
25
|
+
* @param required - Model-visible name, possibly prefixed by the Jini MCP client.
|
|
26
|
+
* @param options - Exact-name lookup when a local tool index is available.
|
|
27
|
+
* @returns The exact name when registered, otherwise the undecorated Jini name.
|
|
28
|
+
* @complexity O(n) for n name characters; pure, no execution authority.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveMcpToolName({ name }: {
|
|
31
|
+
name: string;
|
|
32
|
+
}, { isRegistered }?: {
|
|
33
|
+
isRegistered?: (name: string) => boolean;
|
|
34
|
+
}): string;
|
|
22
35
|
/** True for a plain JSON object (not `null`, not an array). */
|
|
23
36
|
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
24
37
|
/**
|
|
25
|
-
* Copies caller arguments into their canonical wrapper shape before schema validation
|
|
38
|
+
* Copies caller arguments into their canonical wrapper shape before schema validation, reading a
|
|
39
|
+
* canonical integer string as the integer a top-level `integer` property declares.
|
|
26
40
|
* @param requiredArgs - Tool contract and untrusted arguments; neither is mutated.
|
|
27
41
|
* @param _options - Reserved normalization options.
|
|
28
42
|
* @returns Canonical arguments; nested tool input is copied only when fields are folded.
|
|
@@ -33,15 +47,25 @@ export declare function normalizeToolArguments({ tool, rawArgs }: {
|
|
|
33
47
|
tool: McpToolArgumentContract;
|
|
34
48
|
rawArgs: unknown;
|
|
35
49
|
}, _options?: Record<string, never>): Record<string, unknown>;
|
|
50
|
+
/** Attach the complete bounded contract to a repair instruction without echoing input values.
|
|
51
|
+
* @returns The instruction plus schema, or a describe_tool instruction for a large schema.
|
|
52
|
+
* @complexity O(s) serialization time/space for s schema bytes; pure.
|
|
53
|
+
*/
|
|
54
|
+
export declare function withArgumentContract({ message, inputSchema }: {
|
|
55
|
+
message: string;
|
|
56
|
+
inputSchema?: unknown;
|
|
57
|
+
}, _options?: Record<string, never>): string;
|
|
36
58
|
/**
|
|
37
59
|
* Formats structured validation errors as field-specific recovery instructions.
|
|
38
60
|
* @param requiredArgs - Validator errors and, when available, the tool's complete wire input schema.
|
|
39
|
-
* @param
|
|
40
|
-
* @returns A field instruction plus a bounded contract
|
|
61
|
+
* @param options.includeContract - Include the contract for every registry-tool shape error.
|
|
62
|
+
* @returns A field instruction plus a bounded contract when requested or needed for missing fields/items.
|
|
41
63
|
* @complexity O(e + p + s) for e errors, p path characters and s serialized schema bytes.
|
|
42
64
|
*/
|
|
43
65
|
export declare function argumentShapeMessage({ errors, inputSchema }: {
|
|
44
66
|
errors: readonly OutputUnit[];
|
|
45
67
|
inputSchema?: unknown;
|
|
46
|
-
},
|
|
68
|
+
}, { includeContract }?: {
|
|
69
|
+
includeContract?: boolean;
|
|
70
|
+
}): string;
|
|
47
71
|
//# sourceMappingURL=tool-arguments.d.ts.map
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/** Resolve this host's CLI decoration, preserving exact registered names and other servers.
|
|
2
|
+
* @param required - Model-visible name, possibly prefixed by the Jini MCP client.
|
|
3
|
+
* @param options - Exact-name lookup when a local tool index is available.
|
|
4
|
+
* @returns The exact name when registered, otherwise the undecorated Jini name.
|
|
5
|
+
* @complexity O(n) for n name characters; pure, no execution authority.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveMcpToolName({ name }, { isRegistered } = {}) {
|
|
8
|
+
if (isRegistered?.(name))
|
|
9
|
+
return name;
|
|
10
|
+
return name.startsWith('mcp__jini__') ? name.slice('mcp__jini__'.length) : name;
|
|
11
|
+
}
|
|
1
12
|
/** True for a plain JSON object (not `null`, not an array). */
|
|
2
13
|
export function isRecord(value) {
|
|
3
14
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -18,6 +29,24 @@ function applyAliases({ args, aliases }) {
|
|
|
18
29
|
delete args[alias];
|
|
19
30
|
}
|
|
20
31
|
}
|
|
32
|
+
/** A canonical base-10 integer spelling: no sign, padding, leading zero, fraction or exponent. */
|
|
33
|
+
const CANONICAL_INTEGER = /^(?:0|[1-9]\d{0,14})$/;
|
|
34
|
+
/**
|
|
35
|
+
* Replaces each top-level argument that the schema types `integer` and the caller sent as a
|
|
36
|
+
* canonical integer string (`"8"`) with that number, in place. Models do send numbers as strings
|
|
37
|
+
* (Claude Code sent `search_tools {max_results: "8"}` and lost a turn to the refusal); anything
|
|
38
|
+
* else, including `"1.5"` or `"08"`, is left for schema validation to refuse.
|
|
39
|
+
* @complexity O(k) for k argument keys.
|
|
40
|
+
*/
|
|
41
|
+
function coerceIntegerStrings({ args, properties }) {
|
|
42
|
+
for (const [key, value] of Object.entries(args)) {
|
|
43
|
+
const property = properties[key];
|
|
44
|
+
if (typeof value !== 'string' || !isRecord(property) || property.type !== 'integer')
|
|
45
|
+
continue;
|
|
46
|
+
if (CANONICAL_INTEGER.test(value))
|
|
47
|
+
args[key] = Number(value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
21
50
|
/**
|
|
22
51
|
* Moves `extraKeys` from the top level of `args` into its `input` object (created when absent), in place.
|
|
23
52
|
* @throws Error when `input` is not a JSON object, or already holds one of the keys being folded.
|
|
@@ -51,7 +80,8 @@ function assertNoMisplacedInputKey({ args, schema }) {
|
|
|
51
80
|
throw new Error(`move \`input.${misplaced}\` to top-level \`${misplaced}\``);
|
|
52
81
|
}
|
|
53
82
|
/**
|
|
54
|
-
* Copies caller arguments into their canonical wrapper shape before schema validation
|
|
83
|
+
* Copies caller arguments into their canonical wrapper shape before schema validation, reading a
|
|
84
|
+
* canonical integer string as the integer a top-level `integer` property declares.
|
|
55
85
|
* @param requiredArgs - Tool contract and untrusted arguments; neither is mutated.
|
|
56
86
|
* @param _options - Reserved normalization options.
|
|
57
87
|
* @returns Canonical arguments; nested tool input is copied only when fields are folded.
|
|
@@ -64,6 +94,7 @@ export function normalizeToolArguments({ tool, rawArgs }, _options = {}) {
|
|
|
64
94
|
const args = { ...rawArgs };
|
|
65
95
|
const properties = tool.inputSchema.properties ?? {};
|
|
66
96
|
applyAliases({ args, aliases: tool.argumentNormalization?.aliases ?? {} });
|
|
97
|
+
coerceIntegerStrings({ args, properties });
|
|
67
98
|
const extraKeys = Object.keys(args).filter((key) => !Object.hasOwn(properties, key));
|
|
68
99
|
if (tool.argumentNormalization?.foldIntoInput && extraKeys.length > 0) {
|
|
69
100
|
foldExtraKeysIntoInput({ args, extraKeys });
|
|
@@ -107,25 +138,32 @@ function boundedToolInputSchema({ inputSchema }, _options = {}) {
|
|
|
107
138
|
}
|
|
108
139
|
return { inputSchema };
|
|
109
140
|
}
|
|
141
|
+
/** Attach the complete bounded contract to a repair instruction without echoing input values.
|
|
142
|
+
* @returns The instruction plus schema, or a describe_tool instruction for a large schema.
|
|
143
|
+
* @complexity O(s) serialization time/space for s schema bytes; pure.
|
|
144
|
+
*/
|
|
145
|
+
export function withArgumentContract({ message, inputSchema }, _options = {}) {
|
|
146
|
+
const hint = boundedToolInputSchema({ inputSchema });
|
|
147
|
+
const contract = hint.inputSchema === undefined ? hint.schemaNotice : `Input schema: ${JSON.stringify(hint.inputSchema)}`;
|
|
148
|
+
return contract ? `${message}; ${contract}` : message;
|
|
149
|
+
}
|
|
110
150
|
/**
|
|
111
151
|
* Formats structured validation errors as field-specific recovery instructions.
|
|
112
152
|
* @param requiredArgs - Validator errors and, when available, the tool's complete wire input schema.
|
|
113
|
-
* @param
|
|
114
|
-
* @returns A field instruction plus a bounded contract
|
|
153
|
+
* @param options.includeContract - Include the contract for every registry-tool shape error.
|
|
154
|
+
* @returns A field instruction plus a bounded contract when requested or needed for missing fields/items.
|
|
115
155
|
* @complexity O(e + p + s) for e errors, p path characters and s serialized schema bytes.
|
|
116
156
|
*/
|
|
117
|
-
export function argumentShapeMessage({ errors, inputSchema },
|
|
157
|
+
export function argumentShapeMessage({ errors, inputSchema }, { includeContract = false } = {}) {
|
|
118
158
|
// Keep the additionalProperties diagnostic ahead of its generic false-schema child.
|
|
119
159
|
const error = errors.find((entry) => entry.keyword === 'required' || entry.keyword === 'additionalProperties' || entry.instanceLocation !== '#') ?? errors[0];
|
|
120
160
|
if (error === undefined)
|
|
121
161
|
return 'supply arguments matching the declared input schema';
|
|
122
162
|
const path = pointerToFieldPath({ instanceLocation: error.instanceLocation });
|
|
123
163
|
const message = namedFieldMessage({ error, path }) ?? `change \`${path || 'arguments'}\`: ${error.error}`;
|
|
124
|
-
//
|
|
125
|
-
if (!errors.some(entry => ['required', 'items'].includes(entry.keyword)))
|
|
164
|
+
// Wrong keys also need the accepted names and units, not a sequence of remove/retry guesses.
|
|
165
|
+
if (!includeContract && !errors.some(entry => ['required', 'items'].includes(entry.keyword)))
|
|
126
166
|
return message;
|
|
127
|
-
|
|
128
|
-
const contract = hint.inputSchema === undefined ? hint.schemaNotice : `Input schema: ${JSON.stringify(hint.inputSchema)}`;
|
|
129
|
-
return contract ? `${message}; ${contract}` : message;
|
|
167
|
+
return withArgumentContract({ message, inputSchema });
|
|
130
168
|
}
|
|
131
169
|
//# sourceMappingURL=tool-arguments.js.map
|