stitchkit 0.36.1 → 0.37.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/README.md +4 -2
- package/dist/browser/http.d.ts +2 -2
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/contract/define.d.ts +28 -2
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/{index-dvrn81q4.js → index-4pvtq6h2.js} +83 -34
- package/dist/{index-0ed3bx43.js → index-frfyw9fa.js} +2 -2
- package/dist/{index-pmftwk2a.js → index-g3jrbd0z.js} +26 -0
- package/dist/{index-82gncajj.js → index-s6yhmg1k.js} +555 -337
- package/dist/{index-bx49hskg.js → index-xq45akyd.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +7 -3
- package/dist/observability/index.js +3 -3
- package/dist/server/bun.d.ts +32 -0
- package/dist/server/bun.d.ts.map +1 -0
- package/dist/server/context.d.ts +1 -1
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/create.d.ts +2 -3
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +25 -5
- package/dist/server/logger.d.ts +5 -5
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/logging.d.ts +4 -1
- package/dist/server/logging.d.ts.map +1 -1
- package/dist/server/middleware/auth.d.ts +2 -2
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/server/request-body.d.ts +4 -0
- package/dist/server/request-body.d.ts.map +1 -0
- package/dist/server/router.d.ts +3 -3
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/socket-io-config.d.ts +20 -0
- package/dist/server/socket-io-config.d.ts.map +1 -0
- package/dist/server/socket-io-node.d.ts +14 -0
- package/dist/server/socket-io-node.d.ts.map +1 -0
- package/dist/server/socket-io.d.ts +5 -32
- package/dist/server/socket-io.d.ts.map +1 -1
- package/dist/server/types.d.ts +54 -49
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/execute.d.ts +46 -16
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/flatten-join.d.ts +8 -0
- package/dist/tools/flatten-join.d.ts.map +1 -0
- package/dist/tools/flatten.d.ts +7 -44
- package/dist/tools/flatten.d.ts.map +1 -1
- package/dist/tools/json-schema.d.ts +1 -1
- package/dist/tools/json-schema.d.ts.map +1 -1
- package/dist/tools/manifest.d.ts.map +1 -1
- package/dist/tools/mcp-handler.d.ts +7 -13
- package/dist/tools/mcp-handler.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +61 -24
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount.d.ts +9 -6
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/names.d.ts +1 -1
- package/dist/tools/native-mcp.d.ts +57 -0
- package/dist/tools/native-mcp.d.ts.map +1 -0
- package/dist/tools/portable-formats.d.ts +15 -0
- package/dist/tools/portable-formats.d.ts.map +1 -0
- package/dist/tools/presentation.d.ts +15 -0
- package/dist/tools/presentation.d.ts.map +1 -0
- package/dist/tools/remote.d.ts.map +1 -1
- package/dist/tools/schema.d.ts +3 -8
- package/dist/tools/schema.d.ts.map +1 -1
- package/dist/tools/tool-logger.d.ts.map +1 -1
- package/dist/tools/view-file.d.ts +5 -3
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools.d.ts +7 -4
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +357 -95
- package/llms-full.txt +371 -77
- package/package.json +14 -11
- /package/dist/{index-17rdjw68.js → index-e497hxcy.js} +0 -0
package/dist/tools.js
CHANGED
|
@@ -2,42 +2,46 @@ import {
|
|
|
2
2
|
inputIsQuery,
|
|
3
3
|
signJwt,
|
|
4
4
|
verifyPkce
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-xq45akyd.js";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_CORS_ALLOW_HEADERS
|
|
8
8
|
} from "./index-czmqks7r.js";
|
|
9
9
|
import {
|
|
10
10
|
assertToolName,
|
|
11
11
|
assertUniqueToolName,
|
|
12
|
+
buildToolPresentationSchema,
|
|
12
13
|
coerceJsonArgs,
|
|
13
14
|
collectTools,
|
|
14
15
|
createCli,
|
|
15
16
|
createToolRunner,
|
|
17
|
+
executeToolMethod,
|
|
16
18
|
fetchGuarded,
|
|
17
|
-
|
|
18
|
-
flattenUnionsDeep,
|
|
19
|
+
flattenToolJsonSchema,
|
|
19
20
|
formatToolError,
|
|
21
|
+
isObjectPresentationSchema,
|
|
20
22
|
pollUntil,
|
|
23
|
+
presentationMetadata,
|
|
21
24
|
readCapped,
|
|
22
25
|
toolResultFromError,
|
|
23
26
|
writeDownload
|
|
24
|
-
} from "./index-
|
|
27
|
+
} from "./index-s6yhmg1k.js";
|
|
25
28
|
import {
|
|
26
29
|
toJsonSchema
|
|
27
|
-
} from "./index-
|
|
30
|
+
} from "./index-frfyw9fa.js";
|
|
28
31
|
import {
|
|
29
32
|
isWithinDir
|
|
30
33
|
} from "./index-x3fcszf8.js";
|
|
31
34
|
import {
|
|
32
35
|
AppError,
|
|
36
|
+
formatZodError,
|
|
33
37
|
getTraceId,
|
|
34
38
|
isRecord,
|
|
35
39
|
mergeMeta,
|
|
36
40
|
typedEntries
|
|
37
|
-
} from "./index-
|
|
41
|
+
} from "./index-e497hxcy.js";
|
|
38
42
|
|
|
39
43
|
// src/tools/agent.ts
|
|
40
|
-
import {
|
|
44
|
+
import { jsonSchema, tool } from "ai";
|
|
41
45
|
function mountAgent(services, config = {}) {
|
|
42
46
|
const serviceList = Array.isArray(services) ? services : [services];
|
|
43
47
|
const tools = {};
|
|
@@ -59,7 +63,9 @@ function mountAgent(services, config = {}) {
|
|
|
59
63
|
assertUniqueToolName(mountable.name, Object.hasOwn(tools, mountable.name), "agent tool name");
|
|
60
64
|
tools[mountable.name] = tool({
|
|
61
65
|
description: mountable.method.desc,
|
|
62
|
-
inputSchema:
|
|
66
|
+
inputSchema: jsonSchema(mountable.presentationSchema, {
|
|
67
|
+
validate: async (value) => isRecord(value) ? { success: true, value } : { success: false, error: new Error("Tool arguments must be an object") }
|
|
68
|
+
}),
|
|
63
69
|
execute: async (rawArgs) => {
|
|
64
70
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
65
71
|
try {
|
|
@@ -102,19 +108,15 @@ function listToolNames(services) {
|
|
|
102
108
|
}
|
|
103
109
|
// src/tools/manifest.ts
|
|
104
110
|
function buildToolManifest(tools) {
|
|
105
|
-
return tools.map((
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
inputSchema = {};
|
|
111
|
-
}
|
|
112
|
-
return { name: t.name, description: t.method.desc, inputSchema };
|
|
113
|
-
});
|
|
111
|
+
return tools.map((tool2) => ({
|
|
112
|
+
name: tool2.name,
|
|
113
|
+
description: tool2.method.desc,
|
|
114
|
+
inputSchema: tool2.presentationSchema
|
|
115
|
+
}));
|
|
114
116
|
}
|
|
115
117
|
// src/tools/mcp.ts
|
|
116
118
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
117
|
-
import { z } from "zod";
|
|
119
|
+
import { z as z2 } from "zod";
|
|
118
120
|
|
|
119
121
|
// src/tools/mcp-app.ts
|
|
120
122
|
import { readFileSync } from "node:fs";
|
|
@@ -137,6 +139,192 @@ function inlineMcpAppBundle(html) {
|
|
|
137
139
|
return html.replace(EXT_APPS_BUNDLE_PLACEHOLDER, () => bundle);
|
|
138
140
|
}
|
|
139
141
|
|
|
142
|
+
// src/tools/native-mcp.ts
|
|
143
|
+
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
144
|
+
import { z } from "zod";
|
|
145
|
+
function operationIdentity(definition) {
|
|
146
|
+
return {
|
|
147
|
+
method: definition.identity.method,
|
|
148
|
+
desc: definition.description,
|
|
149
|
+
serviceName: definition.identity.serviceName,
|
|
150
|
+
key: definition.identity.action,
|
|
151
|
+
toolName: definition.name,
|
|
152
|
+
scope: definition.identity.scope,
|
|
153
|
+
meta: definition.identity.meta,
|
|
154
|
+
annotations: definition.annotations,
|
|
155
|
+
ui: definition.ui
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function isCallToolResult(value) {
|
|
159
|
+
return CallToolResultSchema.safeParse(value).success;
|
|
160
|
+
}
|
|
161
|
+
function nativeResultSchema(output) {
|
|
162
|
+
const envelope = z.custom(isCallToolResult, {
|
|
163
|
+
error: "Native MCP handler must return a valid CallToolResult"
|
|
164
|
+
});
|
|
165
|
+
if (!output)
|
|
166
|
+
return envelope;
|
|
167
|
+
return envelope.transform((result, context) => {
|
|
168
|
+
const parsed = output.safeParse(result.structuredContent);
|
|
169
|
+
if (!parsed.success) {
|
|
170
|
+
context.addIssue({
|
|
171
|
+
code: "custom",
|
|
172
|
+
message: `Invalid structuredContent: ${formatZodError(parsed.error)}`
|
|
173
|
+
});
|
|
174
|
+
return z.NEVER;
|
|
175
|
+
}
|
|
176
|
+
return { ...result, structuredContent: parsed.data };
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function createNativeMcpRegistrar(server, config) {
|
|
180
|
+
return {
|
|
181
|
+
rawServer: server,
|
|
182
|
+
registerTool: (definition) => {
|
|
183
|
+
const identity = operationIdentity(definition);
|
|
184
|
+
const advertisedOperation = {
|
|
185
|
+
...identity,
|
|
186
|
+
inputSchema: definition.input,
|
|
187
|
+
outputSchema: definition.output,
|
|
188
|
+
handler: () => {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const mountable = {
|
|
193
|
+
method: advertisedOperation,
|
|
194
|
+
name: definition.name,
|
|
195
|
+
argumentSchema: definition.input,
|
|
196
|
+
presentationSchema: buildToolPresentationSchema({
|
|
197
|
+
inputSchema: definition.input,
|
|
198
|
+
unrepresentable: "any"
|
|
199
|
+
}),
|
|
200
|
+
shouldExtend: false
|
|
201
|
+
};
|
|
202
|
+
const [prepared] = config.prepare(mountable);
|
|
203
|
+
if (!prepared)
|
|
204
|
+
return;
|
|
205
|
+
const executionOperation = {
|
|
206
|
+
...identity,
|
|
207
|
+
inputSchema: definition.input,
|
|
208
|
+
outputSchema: nativeResultSchema(definition.output),
|
|
209
|
+
handler: definition.handler
|
|
210
|
+
};
|
|
211
|
+
const toolConfig = {
|
|
212
|
+
description: definition.description,
|
|
213
|
+
inputSchema: z.looseObject({}).meta(presentationMetadata(prepared.inputSchema))
|
|
214
|
+
};
|
|
215
|
+
if (prepared.outputSchema)
|
|
216
|
+
toolConfig.outputSchema = prepared.outputSchema;
|
|
217
|
+
if (definition.annotations)
|
|
218
|
+
toolConfig.annotations = definition.annotations;
|
|
219
|
+
if (definition.ui) {
|
|
220
|
+
toolConfig._meta = {
|
|
221
|
+
ui: definition.ui,
|
|
222
|
+
"ui/resourceUri": definition.ui.resourceUri
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
server.registerTool(definition.name, toolConfig, async (rawArgs) => {
|
|
226
|
+
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
227
|
+
const result = await executeToolMethod(executionOperation, definition.name, args, { ...config.context, source: "mcp" }, config.hooks, config.lifecycle, config.coerceJsonArgs ?? true, config.onOutputStrip ? (paths) => config.onOutputStrip?.(definition.name, paths) : undefined);
|
|
228
|
+
if (!result.ok)
|
|
229
|
+
return config.formatError(result, definition.name);
|
|
230
|
+
if (isCallToolResult(result.data))
|
|
231
|
+
return result.data;
|
|
232
|
+
return config.formatError({
|
|
233
|
+
ok: false,
|
|
234
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
235
|
+
details: { message: "Native MCP handler returned an invalid result" }
|
|
236
|
+
}, definition.name);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/tools/portable-formats.ts
|
|
243
|
+
var PORTABLE_JSON_SCHEMA_FORMATS = new Set([
|
|
244
|
+
"date",
|
|
245
|
+
"time",
|
|
246
|
+
"date-time",
|
|
247
|
+
"duration",
|
|
248
|
+
"uri",
|
|
249
|
+
"uri-reference",
|
|
250
|
+
"uri-template",
|
|
251
|
+
"email",
|
|
252
|
+
"hostname",
|
|
253
|
+
"ipv4",
|
|
254
|
+
"ipv6",
|
|
255
|
+
"regex",
|
|
256
|
+
"uuid",
|
|
257
|
+
"json-pointer",
|
|
258
|
+
"relative-json-pointer"
|
|
259
|
+
]);
|
|
260
|
+
function propertyPath(prefix, property) {
|
|
261
|
+
return prefix ? `${prefix}.${property}` : property;
|
|
262
|
+
}
|
|
263
|
+
function keywordPath(prefix, keyword) {
|
|
264
|
+
if (prefix && keyword.startsWith("["))
|
|
265
|
+
return `${prefix}${keyword}`;
|
|
266
|
+
return prefix ? `${prefix}.${keyword}` : keyword;
|
|
267
|
+
}
|
|
268
|
+
function findNonPortableFormats(schema, allowFormats = []) {
|
|
269
|
+
const allowed = new Set([...PORTABLE_JSON_SCHEMA_FORMATS, ...allowFormats]);
|
|
270
|
+
const found = [];
|
|
271
|
+
const pending = [{ value: schema, path: "" }];
|
|
272
|
+
while (pending.length > 0) {
|
|
273
|
+
const current = pending.pop();
|
|
274
|
+
if (!current || !isRecord(current.value))
|
|
275
|
+
continue;
|
|
276
|
+
const format = current.value.format;
|
|
277
|
+
if (typeof format === "string" && !allowed.has(format)) {
|
|
278
|
+
found.push({ path: current.path || "(root)", format });
|
|
279
|
+
}
|
|
280
|
+
const properties = current.value.properties;
|
|
281
|
+
if (isRecord(properties)) {
|
|
282
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
283
|
+
pending.push({ value, path: propertyPath(current.path, key) });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
for (const mapKey of ["$defs", "definitions", "patternProperties", "dependentSchemas"]) {
|
|
287
|
+
const entries = current.value[mapKey];
|
|
288
|
+
if (!isRecord(entries))
|
|
289
|
+
continue;
|
|
290
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
291
|
+
pending.push({ value, path: keywordPath(current.path, `${mapKey}.${key}`) });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const directChildren = [
|
|
295
|
+
["items", "[]"],
|
|
296
|
+
["contains", "contains"],
|
|
297
|
+
["additionalProperties", "*"],
|
|
298
|
+
["unevaluatedProperties", "*"],
|
|
299
|
+
["propertyNames", "propertyNames"],
|
|
300
|
+
["not", "not"],
|
|
301
|
+
["if", "if"],
|
|
302
|
+
["then", "then"],
|
|
303
|
+
["else", "else"]
|
|
304
|
+
];
|
|
305
|
+
for (const [keyword, segment] of directChildren) {
|
|
306
|
+
const child = current.value[keyword];
|
|
307
|
+
if (isRecord(child)) {
|
|
308
|
+
pending.push({ value: child, path: keywordPath(current.path, segment) });
|
|
309
|
+
}
|
|
310
|
+
if (Array.isArray(child)) {
|
|
311
|
+
child.forEach((value, index) => {
|
|
312
|
+
pending.push({ value, path: keywordPath(current.path, `${segment}[${index}]`) });
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const keyword of ["prefixItems", "allOf", "anyOf", "oneOf"]) {
|
|
317
|
+
const branches = current.value[keyword];
|
|
318
|
+
if (!Array.isArray(branches))
|
|
319
|
+
continue;
|
|
320
|
+
branches.forEach((value, index) => {
|
|
321
|
+
pending.push({ value, path: keywordPath(current.path, `${keyword}[${index}]`) });
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return found.sort((left, right) => left.path.localeCompare(right.path) || left.format.localeCompare(right.format));
|
|
326
|
+
}
|
|
327
|
+
|
|
140
328
|
// src/tools/untyped-properties.ts
|
|
141
329
|
var TYPE_KEYWORDS = ["type", "enum", "const", "anyOf", "oneOf", "allOf", "$ref", "not"];
|
|
142
330
|
function saysWhatItIs(schema) {
|
|
@@ -239,10 +427,10 @@ function probeSchema(schema, io) {
|
|
|
239
427
|
function resolveOutputSchema(outputSchema) {
|
|
240
428
|
if (!outputSchema)
|
|
241
429
|
return null;
|
|
242
|
-
if (outputSchema instanceof
|
|
430
|
+
if (outputSchema instanceof z2.ZodObject) {
|
|
243
431
|
return { schema: outputSchema, mode: "direct" };
|
|
244
432
|
}
|
|
245
|
-
return { schema:
|
|
433
|
+
return { schema: z2.object({ result: outputSchema }), mode: "wrapped" };
|
|
246
434
|
}
|
|
247
435
|
function reportIncompatible(message, policy, logger, failures) {
|
|
248
436
|
if (policy === "throw") {
|
|
@@ -261,51 +449,99 @@ function throwIfFailures(failures) {
|
|
|
261
449
|
- `)}`);
|
|
262
450
|
}
|
|
263
451
|
}
|
|
264
|
-
function prepareMcpTool(mountable,
|
|
452
|
+
function prepareMcpTool(mountable, config, validation, logger, failures, seen) {
|
|
453
|
+
const policy = validation.policy ?? "throw";
|
|
265
454
|
assertUniqueToolName(mountable.name, seen.has(mountable.name), "MCP tool name");
|
|
266
455
|
seen.add(mountable.name);
|
|
267
|
-
|
|
268
|
-
|
|
456
|
+
let inputJsonSchema;
|
|
457
|
+
try {
|
|
458
|
+
inputJsonSchema = buildToolPresentationSchema({
|
|
459
|
+
paramsSchema: mountable.method.paramsSchema,
|
|
460
|
+
inputSchema: mountable.method.inputSchema,
|
|
461
|
+
extendSchema: mountable.shouldExtend && config.extend ? config.extend.schema : undefined,
|
|
462
|
+
flattenUnionInput: config.flattenUnionInput,
|
|
463
|
+
unrepresentable: "throw"
|
|
464
|
+
});
|
|
465
|
+
} catch (err) {
|
|
466
|
+
reportIncompatible(`MCP tool "${mountable.name}" — input schema is not JSON Schema-compatible: ${err instanceof Error ? err.message : String(err)}`, policy, logger, failures);
|
|
269
467
|
return null;
|
|
270
468
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
reportIncompatible(`MCP tool "${mountable.name}" — input schema is not JSON Schema-compatible: ${inputError}`, policy, logger, failures);
|
|
469
|
+
if (!isObjectPresentationSchema(inputJsonSchema)) {
|
|
470
|
+
reportIncompatible(`MCP tool "${mountable.name}" — input must be an object schema; a union, discriminated union or scalar cannot be an MCP tool input (flatten it in the contract, or drop MCP from \`expose\`)`, policy, logger, failures);
|
|
274
471
|
return null;
|
|
275
472
|
}
|
|
473
|
+
if (validation.requireTypedProperties) {
|
|
474
|
+
const allowed = new Set(validation.allowUntyped ?? []);
|
|
475
|
+
for (const untyped of findUntypedProperties(inputJsonSchema)) {
|
|
476
|
+
const path = `${mountable.name}.${untyped.path}`;
|
|
477
|
+
if (allowed.has(path))
|
|
478
|
+
continue;
|
|
479
|
+
const clue = untyped.description ? ` (only a description: "${untyped.description}")` : "";
|
|
480
|
+
reportIncompatible(`MCP tool "${mountable.name}" — input property "${untyped.path}" carries no type, enum or $ref${clue}. ` + "A model is given no way to know what to send. Widen the contract, or list it in `allowUntyped` if it is deliberately free-form.", policy === "skip" ? "warn" : policy, logger, failures);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (validation.requirePortableFormats) {
|
|
484
|
+
for (const finding of findNonPortableFormats(inputJsonSchema, validation.allowFormats)) {
|
|
485
|
+
reportIncompatible(`MCP tool "${mountable.name}" — input property "${finding.path}" uses non-portable JSON Schema format "${finding.format}". ` + "Use a portable pattern/schema, or list the format in `allowFormats` only when every MCP client supports it.", policy === "skip" ? "warn" : policy, logger, failures);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
276
488
|
const resolved = resolveOutputSchema(mountable.method.outputSchema);
|
|
277
489
|
if (!resolved)
|
|
278
|
-
return { outputMode: "none" };
|
|
490
|
+
return { mountable, inputSchema: inputJsonSchema, outputMode: "none" };
|
|
279
491
|
const outputError = probeSchema(resolved.schema, "output");
|
|
280
492
|
if (outputError) {
|
|
281
493
|
reportIncompatible(`MCP tool "${mountable.name}" — output schema is not JSON Schema-compatible: ${outputError}`, policy, logger, failures);
|
|
282
|
-
return { outputMode: "none" };
|
|
494
|
+
return { mountable, inputSchema: inputJsonSchema, outputMode: "none" };
|
|
495
|
+
}
|
|
496
|
+
if (validation.requirePortableFormats) {
|
|
497
|
+
for (const finding of findNonPortableFormats(toJsonSchema(resolved.schema, "output"), validation.allowFormats)) {
|
|
498
|
+
reportIncompatible(`MCP tool "${mountable.name}" — output property "${finding.path}" uses non-portable JSON Schema format "${finding.format}". ` + "Use a portable pattern/schema, or list the format in `allowFormats` only when every MCP client supports it.", policy === "skip" ? "warn" : policy, logger, failures);
|
|
499
|
+
}
|
|
283
500
|
}
|
|
284
|
-
return {
|
|
501
|
+
return {
|
|
502
|
+
mountable,
|
|
503
|
+
inputSchema: inputJsonSchema,
|
|
504
|
+
outputSchema: resolved.schema,
|
|
505
|
+
outputMode: resolved.mode
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function prepareMcpSurface(services, config = {}) {
|
|
509
|
+
const serviceList = Array.isArray(services) ? services : [services];
|
|
510
|
+
const tools = serviceList.flatMap((service) => collectTools(service, "MCP", config));
|
|
511
|
+
return prepareMcpTools(tools, config);
|
|
285
512
|
}
|
|
286
|
-
function
|
|
513
|
+
function prepareMcpTools(tools, config = {}) {
|
|
287
514
|
const seen = new Set;
|
|
288
515
|
const failures = [];
|
|
289
|
-
const
|
|
290
|
-
for (const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
for (const untyped of findUntypedProperties(toJsonSchema(mountable.schema, "input"))) {
|
|
296
|
-
const path = `${mountable.name}.${untyped.path}`;
|
|
297
|
-
if (allowed.has(path))
|
|
298
|
-
continue;
|
|
299
|
-
const clue = untyped.description ? ` (only a description: "${untyped.description}")` : "";
|
|
300
|
-
reportIncompatible(`MCP tool "${mountable.name}" — property "${untyped.path}" carries no type, enum or $ref${clue}. ` + "A model is given no way to know what to send. Widen the contract, or list it in `allowUntyped` if it is deliberately free-form.", onIncompatibleSchema === "skip" ? "warn" : onIncompatibleSchema, logger, failures);
|
|
301
|
-
}
|
|
516
|
+
const prepared = [];
|
|
517
|
+
for (const mountable of tools) {
|
|
518
|
+
const tool2 = prepareMcpTool(mountable, config, config.schemaValidation ?? {}, config.logger, failures, seen);
|
|
519
|
+
if (tool2) {
|
|
520
|
+
Object.freeze(tool2.mountable);
|
|
521
|
+
prepared.push(Object.freeze(tool2));
|
|
302
522
|
}
|
|
303
523
|
}
|
|
304
524
|
throwIfFailures(failures);
|
|
525
|
+
return Object.freeze(prepared);
|
|
526
|
+
}
|
|
527
|
+
function validateMcpSchemas(config) {
|
|
528
|
+
prepareMcpSurface(config.services, {
|
|
529
|
+
extend: config.extend,
|
|
530
|
+
flattenUnionInput: config.flattenUnionInput,
|
|
531
|
+
schemaValidation: config,
|
|
532
|
+
logger: config.logger
|
|
533
|
+
});
|
|
305
534
|
}
|
|
306
535
|
function mountMcp(mcpServer, services, config = {}) {
|
|
307
|
-
const
|
|
308
|
-
|
|
536
|
+
const prepared = prepareMcpSurface(services, {
|
|
537
|
+
extend: config.extend,
|
|
538
|
+
flattenUnionInput: config.flattenUnionInput,
|
|
539
|
+
schemaValidation: config.schemaValidation,
|
|
540
|
+
logger: config.logger
|
|
541
|
+
});
|
|
542
|
+
mountPreparedMcp(mcpServer, prepared, config);
|
|
543
|
+
}
|
|
544
|
+
function mountPreparedMcp(mcpServer, prepared, config = {}) {
|
|
309
545
|
const runTool = createToolRunner({
|
|
310
546
|
source: "mcp",
|
|
311
547
|
extend: config.extend,
|
|
@@ -316,57 +552,74 @@ function mountMcp(mcpServer, services, config = {}) {
|
|
|
316
552
|
coerceJsonArgs: config.coerceJsonArgs,
|
|
317
553
|
onOutputStrip: config.onOutputStrip
|
|
318
554
|
});
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
if (mountable.method.ui) {
|
|
336
|
-
toolConfig._meta = {
|
|
337
|
-
ui: mountable.method.ui,
|
|
338
|
-
"ui/resourceUri": mountable.method.ui.resourceUri
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
mcpServer.registerTool(mountable.name, toolConfig, async (rawArgs) => {
|
|
342
|
-
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
343
|
-
try {
|
|
344
|
-
const result = await runTool(mountable, args);
|
|
345
|
-
return formatMcpResult(result, prepared.outputMode, mountable.name, config.errorHint);
|
|
346
|
-
} catch (err) {
|
|
347
|
-
return formatMcpResult(toolResultFromError(err), "none", mountable.name, config.errorHint);
|
|
348
|
-
}
|
|
349
|
-
});
|
|
555
|
+
for (const descriptor of prepared) {
|
|
556
|
+
const { mountable } = descriptor;
|
|
557
|
+
const toolConfig = {
|
|
558
|
+
description: mountable.method.desc,
|
|
559
|
+
inputSchema: z2.looseObject({}).meta(presentationMetadata(descriptor.inputSchema))
|
|
560
|
+
};
|
|
561
|
+
if (descriptor.outputSchema)
|
|
562
|
+
toolConfig.outputSchema = descriptor.outputSchema;
|
|
563
|
+
if (mountable.method.annotations) {
|
|
564
|
+
toolConfig.annotations = mountable.method.annotations;
|
|
565
|
+
}
|
|
566
|
+
if (mountable.method.ui) {
|
|
567
|
+
toolConfig._meta = {
|
|
568
|
+
ui: mountable.method.ui,
|
|
569
|
+
"ui/resourceUri": mountable.method.ui.resourceUri
|
|
570
|
+
};
|
|
350
571
|
}
|
|
572
|
+
mcpServer.registerTool(mountable.name, toolConfig, async (rawArgs) => {
|
|
573
|
+
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
574
|
+
try {
|
|
575
|
+
const result = await runTool(mountable, args);
|
|
576
|
+
return formatMcpResult(result, descriptor.outputMode, mountable.name, config.errorHint);
|
|
577
|
+
} catch (err) {
|
|
578
|
+
return formatMcpResult(toolResultFromError(err), "none", mountable.name, config.errorHint);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
351
581
|
}
|
|
352
|
-
throwIfFailures(failures);
|
|
353
582
|
}
|
|
354
583
|
function buildMcpServer(config, auth) {
|
|
355
|
-
const server = new McpServer(config.serverInfo, config.instructions ? { instructions: config.instructions } : undefined);
|
|
356
584
|
const services = typeof config.services === "function" ? config.services(auth) : config.services;
|
|
585
|
+
const prepared = prepareMcpSurface(services, {
|
|
586
|
+
extend: config.extend,
|
|
587
|
+
flattenUnionInput: config.flattenUnionInput,
|
|
588
|
+
schemaValidation: config.schemaValidation,
|
|
589
|
+
logger: config.logger
|
|
590
|
+
});
|
|
591
|
+
return buildMcpServerFromPrepared(config, auth, prepared);
|
|
592
|
+
}
|
|
593
|
+
function buildMcpServerFromPrepared(config, auth, prepared) {
|
|
594
|
+
const server = new McpServer(config.serverInfo, config.instructions ? { instructions: config.instructions } : undefined);
|
|
357
595
|
const context = config.context?.(auth);
|
|
358
|
-
|
|
596
|
+
mountPreparedMcp(server, prepared, {
|
|
359
597
|
context,
|
|
360
598
|
hooks: config.hooks,
|
|
361
599
|
lifecycle: config.lifecycle,
|
|
362
600
|
extend: config.extend,
|
|
363
|
-
|
|
601
|
+
schemaValidation: config.schemaValidation,
|
|
364
602
|
logger: config.logger,
|
|
365
603
|
coerceJsonArgs: config.coerceJsonArgs,
|
|
366
604
|
flattenUnionInput: config.flattenUnionInput,
|
|
367
|
-
errorHint: config.errorHint
|
|
605
|
+
errorHint: config.errorHint,
|
|
606
|
+
onOutputStrip: config.onOutputStrip
|
|
368
607
|
});
|
|
369
|
-
config.nativeTools
|
|
608
|
+
if (config.nativeTools) {
|
|
609
|
+
const registrar = createNativeMcpRegistrar(server, {
|
|
610
|
+
context,
|
|
611
|
+
hooks: config.hooks,
|
|
612
|
+
lifecycle: config.lifecycle,
|
|
613
|
+
coerceJsonArgs: config.coerceJsonArgs,
|
|
614
|
+
onOutputStrip: config.onOutputStrip,
|
|
615
|
+
prepare: (tool2) => prepareMcpTools([tool2], {
|
|
616
|
+
schemaValidation: config.schemaValidation,
|
|
617
|
+
logger: config.logger
|
|
618
|
+
}),
|
|
619
|
+
formatError: (result, toolName) => formatMcpResult(result, "none", toolName, config.errorHint)
|
|
620
|
+
});
|
|
621
|
+
config.nativeTools(registrar, auth);
|
|
622
|
+
}
|
|
370
623
|
for (const resource of config.resources ?? []) {
|
|
371
624
|
mountMcpResource(server, resource);
|
|
372
625
|
}
|
|
@@ -477,17 +730,23 @@ function jsonRpcError(code, message, status, headers) {
|
|
|
477
730
|
return Response.json({ jsonrpc: "2.0", error: { code, message }, id: null }, { status, headers });
|
|
478
731
|
}
|
|
479
732
|
function createMcpHandler(config) {
|
|
733
|
+
let buildServer;
|
|
480
734
|
if (Array.isArray(config.services)) {
|
|
481
|
-
|
|
735
|
+
const prepared = prepareMcpSurface(config.services, {
|
|
736
|
+
logger: config.logger,
|
|
482
737
|
extend: config.extend,
|
|
483
|
-
flattenUnionInput: config.flattenUnionInput
|
|
738
|
+
flattenUnionInput: config.flattenUnionInput,
|
|
739
|
+
schemaValidation: config.schemaValidation
|
|
484
740
|
});
|
|
741
|
+
buildServer = (auth) => buildMcpServerFromPrepared(config, auth, prepared);
|
|
742
|
+
} else {
|
|
743
|
+
buildServer = (auth) => buildMcpServer(config, auth);
|
|
485
744
|
}
|
|
486
745
|
const unauthorized = () => {
|
|
487
746
|
const headers = config.protectedResource ? { "WWW-Authenticate": wwwAuthenticateHeader(config.protectedResource.resource) } : undefined;
|
|
488
747
|
return jsonRpcError(-32001, "Authorization required", 401, headers);
|
|
489
748
|
};
|
|
490
|
-
if (config.stateless) {
|
|
749
|
+
if ((config.sessionMode ?? "stateless") === "stateless") {
|
|
491
750
|
return async (req) => {
|
|
492
751
|
const auth = await config.auth(req);
|
|
493
752
|
if (!auth)
|
|
@@ -496,7 +755,7 @@ function createMcpHandler(config) {
|
|
|
496
755
|
sessionIdGenerator: undefined,
|
|
497
756
|
enableJsonResponse: true
|
|
498
757
|
});
|
|
499
|
-
const server =
|
|
758
|
+
const server = buildServer(auth);
|
|
500
759
|
await server.connect(transport);
|
|
501
760
|
return transport.handleRequest(req);
|
|
502
761
|
};
|
|
@@ -555,7 +814,7 @@ function createMcpHandler(config) {
|
|
|
555
814
|
sessionIdGenerator: () => newSessionId,
|
|
556
815
|
eventStore
|
|
557
816
|
});
|
|
558
|
-
const server =
|
|
817
|
+
const server = buildServer(auth);
|
|
559
818
|
sessions.set(newSessionId, { transport, server, lastSeen: Date.now() });
|
|
560
819
|
transport.onclose = () => {
|
|
561
820
|
sessions.delete(newSessionId);
|
|
@@ -644,13 +903,13 @@ function mountDownload(server, config) {
|
|
|
644
903
|
});
|
|
645
904
|
}
|
|
646
905
|
// src/tools/mount-upload.ts
|
|
647
|
-
import { z as
|
|
906
|
+
import { z as z3 } from "zod";
|
|
648
907
|
function mountUpload(server, config) {
|
|
649
908
|
const name = config.name ?? "upload";
|
|
650
909
|
assertToolName(name, "<native>", "upload");
|
|
651
910
|
server.registerTool(name, {
|
|
652
911
|
description: config.description,
|
|
653
|
-
inputSchema: { path:
|
|
912
|
+
inputSchema: { path: z3.string().describe("Path to a local file on this machine") }
|
|
654
913
|
}, async (rawArgs) => {
|
|
655
914
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
656
915
|
const path = typeof args.path === "string" ? args.path : "";
|
|
@@ -1255,7 +1514,7 @@ function implementRemote(contract, http, options) {
|
|
|
1255
1514
|
serviceName: contract.meta.prefix,
|
|
1256
1515
|
key,
|
|
1257
1516
|
toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
|
|
1258
|
-
expose: endpoint.rawResponse ? HTTP_ONLY : endpoint.expose,
|
|
1517
|
+
expose: endpoint.rawResponse || endpoint.rawBody ? HTTP_ONLY : endpoint.expose,
|
|
1259
1518
|
ui: "ui" in endpoint ? endpoint.ui : undefined,
|
|
1260
1519
|
annotations: "annotations" in endpoint ? endpoint.annotations : undefined,
|
|
1261
1520
|
meta: mergeMeta(contract.meta.meta, endpoint.meta),
|
|
@@ -1264,8 +1523,10 @@ function implementRemote(contract, http, options) {
|
|
|
1264
1523
|
inputSchema: endpoint.input,
|
|
1265
1524
|
outputSchema: endpoint.output,
|
|
1266
1525
|
multipart: endpoint.multipart,
|
|
1526
|
+
maxJsonBodyBytes: endpoint.maxJsonBodyBytes,
|
|
1267
1527
|
idempotent: endpoint.idempotent,
|
|
1268
1528
|
rawResponse: endpoint.rawResponse,
|
|
1529
|
+
rawBody: endpoint.rawBody,
|
|
1269
1530
|
contentType: "contentType" in endpoint ? endpoint.contentType : undefined,
|
|
1270
1531
|
handler: async (ctx) => {
|
|
1271
1532
|
const call = client[key];
|
|
@@ -1296,7 +1557,7 @@ function implementRemote(contract, http, options) {
|
|
|
1296
1557
|
function createToolLogger(config = {}) {
|
|
1297
1558
|
const log = config.log ?? ((line) => console.info(line));
|
|
1298
1559
|
return {
|
|
1299
|
-
afterToolCall: (toolName,
|
|
1560
|
+
afterToolCall: ({ toolName, result, durationMs, context, endpoint }) => {
|
|
1300
1561
|
const record = {
|
|
1301
1562
|
tool: toolName,
|
|
1302
1563
|
service: endpoint.serviceName,
|
|
@@ -1350,7 +1611,7 @@ function summarizeTransports(services) {
|
|
|
1350
1611
|
// src/tools/view-file.ts
|
|
1351
1612
|
import { readFile, realpath, stat } from "node:fs/promises";
|
|
1352
1613
|
import { extname as extname2, resolve } from "node:path";
|
|
1353
|
-
import { z as
|
|
1614
|
+
import { z as z4 } from "zod";
|
|
1354
1615
|
var MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
1355
1616
|
var EXT_MIME = {
|
|
1356
1617
|
".png": "image/png",
|
|
@@ -1450,7 +1711,7 @@ function mountViewFile(server, options = {}) {
|
|
|
1450
1711
|
server.registerTool("view_file", {
|
|
1451
1712
|
description: "View media (image, audio, video) by URL or local path — returns it as content you can SEE / HEAR. Pass several paths to view multiple files at once. Use it on a generation `output` url to inspect the result.",
|
|
1452
1713
|
inputSchema: {
|
|
1453
|
-
paths:
|
|
1714
|
+
paths: z4.union([z4.string(), z4.array(z4.string())]).describe("Media URL(s) or file path(s) to view")
|
|
1454
1715
|
},
|
|
1455
1716
|
annotations: { title: "View Media", readOnlyHint: true, idempotentHint: true }
|
|
1456
1717
|
}, async (args) => {
|
|
@@ -1487,9 +1748,9 @@ export {
|
|
|
1487
1748
|
listToolNames,
|
|
1488
1749
|
inlineMcpAppBundle,
|
|
1489
1750
|
implementRemote,
|
|
1490
|
-
|
|
1491
|
-
flattenDiscriminatedUnion,
|
|
1751
|
+
flattenToolJsonSchema,
|
|
1492
1752
|
findUntypedProperties,
|
|
1753
|
+
findNonPortableFormats,
|
|
1493
1754
|
createToolkit,
|
|
1494
1755
|
createToolLogger,
|
|
1495
1756
|
createStdioMcpServer,
|
|
@@ -1501,5 +1762,6 @@ export {
|
|
|
1501
1762
|
buildMcpServer,
|
|
1502
1763
|
RESOURCE_MIME_TYPE,
|
|
1503
1764
|
PROTECTED_RESOURCE_PATH,
|
|
1765
|
+
PORTABLE_JSON_SCHEMA_FORMATS,
|
|
1504
1766
|
EXT_APPS_BUNDLE_PLACEHOLDER
|
|
1505
1767
|
};
|