stitchkit 0.36.1 → 0.38.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/client-url.d.ts +11 -0
- package/dist/browser/client-url.d.ts.map +1 -0
- package/dist/browser/client.d.ts +16 -4
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +7 -3
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/contract/define.d.ts +102 -2
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/{index-bx49hskg.js → index-bgdd42pt.js} +1 -1
- package/dist/{index-0ed3bx43.js → index-frfyw9fa.js} +2 -2
- package/dist/{index-82gncajj.js → index-n5t4gnfz.js} +557 -337
- package/dist/{index-dvrn81q4.js → index-p3kwf73n.js} +165 -55
- package/dist/{index-pmftwk2a.js → index-x4wbc8sz.js} +54 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +147 -114
- 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 +41 -7
- 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/openapi.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/response-metadata.d.ts +9 -0
- package/dist/server/response-metadata.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 +63 -50
- 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 +481 -205
- package/llms-full.txt +536 -87
- package/package.json +14 -11
- /package/dist/{index-17rdjw68.js → index-mzx0an0s.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-bgdd42pt.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-n5t4gnfz.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-mzx0an0s.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
|
-
const toolConfig = { description: mountable.method.desc, inputSchema: mountable.schema };
|
|
330
|
-
if (prepared.outputSchema)
|
|
331
|
-
toolConfig.outputSchema = prepared.outputSchema;
|
|
332
|
-
if (mountable.method.annotations) {
|
|
333
|
-
toolConfig.annotations = mountable.method.annotations;
|
|
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;
|
|
350
565
|
}
|
|
566
|
+
if (mountable.method.ui) {
|
|
567
|
+
toolConfig._meta = {
|
|
568
|
+
ui: mountable.method.ui,
|
|
569
|
+
"ui/resourceUri": mountable.method.ui.resourceUri
|
|
570
|
+
};
|
|
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 : "";
|
|
@@ -976,6 +1235,117 @@ function appendFormFields(formData, values, skipKeys) {
|
|
|
976
1235
|
}
|
|
977
1236
|
}
|
|
978
1237
|
|
|
1238
|
+
// src/browser/client-url.ts
|
|
1239
|
+
function isParamArray(value) {
|
|
1240
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string" || typeof item === "number");
|
|
1241
|
+
}
|
|
1242
|
+
function collectQueryParams(args, endpoint) {
|
|
1243
|
+
const params = {};
|
|
1244
|
+
let hasParams = false;
|
|
1245
|
+
for (const [key, value] of Object.entries(args)) {
|
|
1246
|
+
if (value === undefined || value === null)
|
|
1247
|
+
continue;
|
|
1248
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isParamArray(value)) {
|
|
1249
|
+
params[key] = value;
|
|
1250
|
+
hasParams = true;
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
const what = Array.isArray(value) ? "an array with non-primitive items" : typeof value === "object" ? "a nested object" : `a ${typeof value}`;
|
|
1254
|
+
throw new Error(`${endpoint.method} ${endpoint.path}: input field "${key}" is ${what} — it cannot ` + "travel as a query parameter. GET / DELETE input must be flat (string / number / " + "boolean, or an array of string / number); flatten the field or move the " + "operation to a body verb (POST).");
|
|
1255
|
+
}
|
|
1256
|
+
return hasParams ? params : undefined;
|
|
1257
|
+
}
|
|
1258
|
+
function hasStringKeys(args, keys) {
|
|
1259
|
+
for (const key of keys) {
|
|
1260
|
+
if (typeof args[key] !== "string")
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
return true;
|
|
1264
|
+
}
|
|
1265
|
+
function resolvePathPrefix(config, args) {
|
|
1266
|
+
if (!config?.pathPrefix)
|
|
1267
|
+
return "";
|
|
1268
|
+
if (typeof config.pathPrefix === "string")
|
|
1269
|
+
return config.pathPrefix;
|
|
1270
|
+
const keys = config.stripPrefixKeys ?? [];
|
|
1271
|
+
if (!hasStringKeys(args, keys)) {
|
|
1272
|
+
const missing = keys.find((key) => typeof args[key] !== "string");
|
|
1273
|
+
throw new Error(`Missing path prefix key: ${missing}`);
|
|
1274
|
+
}
|
|
1275
|
+
return config.pathPrefix(args);
|
|
1276
|
+
}
|
|
1277
|
+
function extractParamNames(path) {
|
|
1278
|
+
const matches = path.match(/:(\w+)/g);
|
|
1279
|
+
const names = matches ? matches.map((match) => match.slice(1)) : [];
|
|
1280
|
+
if (path.endsWith("/*"))
|
|
1281
|
+
names.push("*");
|
|
1282
|
+
return names;
|
|
1283
|
+
}
|
|
1284
|
+
function fillPathParams(path, args) {
|
|
1285
|
+
let filled = path.replace(/:(\w+)/g, (_, key) => {
|
|
1286
|
+
const value = args[key];
|
|
1287
|
+
if (value === undefined || value === null) {
|
|
1288
|
+
throw new Error(`Missing path param: ${key}`);
|
|
1289
|
+
}
|
|
1290
|
+
return encodeURIComponent(String(value));
|
|
1291
|
+
});
|
|
1292
|
+
if (!filled.endsWith("/*"))
|
|
1293
|
+
return filled;
|
|
1294
|
+
const wildcard = args["*"];
|
|
1295
|
+
if (wildcard === undefined || wildcard === null) {
|
|
1296
|
+
throw new Error("Missing path param: *");
|
|
1297
|
+
}
|
|
1298
|
+
const remainder = String(wildcard).split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
1299
|
+
filled = `${filled.slice(0, -1)}${remainder}`;
|
|
1300
|
+
return filled;
|
|
1301
|
+
}
|
|
1302
|
+
function stripConsumedArgs(args, path, scopeKeys) {
|
|
1303
|
+
const consumed = new Set(scopeKeys);
|
|
1304
|
+
for (const name of extractParamNames(path))
|
|
1305
|
+
consumed.add(name);
|
|
1306
|
+
const remaining = {};
|
|
1307
|
+
for (const [key, value] of Object.entries(args)) {
|
|
1308
|
+
if (!consumed.has(key) && value !== undefined)
|
|
1309
|
+
remaining[key] = value;
|
|
1310
|
+
}
|
|
1311
|
+
return remaining;
|
|
1312
|
+
}
|
|
1313
|
+
function appendQuery(relativeUrl, params) {
|
|
1314
|
+
if (!params)
|
|
1315
|
+
return relativeUrl;
|
|
1316
|
+
const search = new URLSearchParams;
|
|
1317
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1318
|
+
if (Array.isArray(value)) {
|
|
1319
|
+
for (const item of value)
|
|
1320
|
+
search.append(key, String(item));
|
|
1321
|
+
} else {
|
|
1322
|
+
search.set(key, String(value));
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return search.size > 0 ? `${relativeUrl}?${search}` : relativeUrl;
|
|
1326
|
+
}
|
|
1327
|
+
function planClientRequest(endpoint, contractPrefix, args, config) {
|
|
1328
|
+
let pathPrefix = resolvePathPrefix(config, args);
|
|
1329
|
+
if (pathPrefix && !pathPrefix.endsWith("/"))
|
|
1330
|
+
pathPrefix += "/";
|
|
1331
|
+
if (pathPrefix.startsWith("/"))
|
|
1332
|
+
pathPrefix = pathPrefix.slice(1);
|
|
1333
|
+
const endpointPath = endpoint.path === "/" ? "" : endpoint.path;
|
|
1334
|
+
let relativeUrl = fillPathParams(`${pathPrefix}${contractPrefix}${endpointPath}`, args);
|
|
1335
|
+
if (relativeUrl.endsWith("/"))
|
|
1336
|
+
relativeUrl = relativeUrl.slice(0, -1);
|
|
1337
|
+
const remainingArgs = stripConsumedArgs(args, endpoint.path, config?.stripPrefixKeys ?? []);
|
|
1338
|
+
if (inputIsQuery(endpoint.method)) {
|
|
1339
|
+
relativeUrl = appendQuery(relativeUrl, collectQueryParams(remainingArgs, endpoint));
|
|
1340
|
+
}
|
|
1341
|
+
return { relativeUrl, remainingArgs };
|
|
1342
|
+
}
|
|
1343
|
+
function joinClientBaseUrl(baseUrl, relativeUrl) {
|
|
1344
|
+
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
1345
|
+
const path = relativeUrl.startsWith("/") ? relativeUrl : `/${relativeUrl}`;
|
|
1346
|
+
return `${base}${path}`;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
979
1349
|
// src/browser/http.ts
|
|
980
1350
|
import ky, { isHTTPError } from "ky";
|
|
981
1351
|
class ApiError extends Error {
|
|
@@ -1026,25 +1396,6 @@ function withOutput(endpoint, result) {
|
|
|
1026
1396
|
return result;
|
|
1027
1397
|
return result.then((value) => value === undefined ? undefined : schema.parse(value));
|
|
1028
1398
|
}
|
|
1029
|
-
function isParamArray(value) {
|
|
1030
|
-
return Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number");
|
|
1031
|
-
}
|
|
1032
|
-
function collectQueryParams(args, skipKeys, endpoint) {
|
|
1033
|
-
const params = {};
|
|
1034
|
-
let hasParams = false;
|
|
1035
|
-
for (const [key, value] of Object.entries(args)) {
|
|
1036
|
-
if (skipKeys.has(key) || value === undefined || value === null)
|
|
1037
|
-
continue;
|
|
1038
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isParamArray(value)) {
|
|
1039
|
-
params[key] = value;
|
|
1040
|
-
hasParams = true;
|
|
1041
|
-
continue;
|
|
1042
|
-
}
|
|
1043
|
-
const what = Array.isArray(value) ? "an array with non-primitive items" : typeof value === "object" ? "a nested object" : `a ${typeof value}`;
|
|
1044
|
-
throw new Error(`${endpoint.method} ${endpoint.path}: input field "${key}" is ${what} — it cannot ` + "travel as a query parameter. GET / DELETE input must be flat (string / number / " + "boolean, or an array of string / number); flatten the field or move the " + "operation to a body verb (POST).");
|
|
1045
|
-
}
|
|
1046
|
-
return hasParams ? params : undefined;
|
|
1047
|
-
}
|
|
1048
1399
|
function createClient(contract, configOrClient, contractConfig) {
|
|
1049
1400
|
const client = {};
|
|
1050
1401
|
const makeMethod = isHttpAdapter(configOrClient) ? (endpoint) => createHttpMethod(endpoint, contract.meta.prefix, configOrClient, contractConfig) : (endpoint) => createFetchMethod(endpoint, contract.meta.prefix, configOrClient, contractConfig);
|
|
@@ -1063,27 +1414,9 @@ function setClientMethod(target, key, method) {
|
|
|
1063
1414
|
}
|
|
1064
1415
|
function createHttpMethod(endpoint, prefix, client, config) {
|
|
1065
1416
|
const httpMethod = endpoint.method.toLowerCase();
|
|
1066
|
-
const isGet = httpMethod === "get";
|
|
1067
|
-
const paramNames = extractParamNames(endpoint.path);
|
|
1068
|
-
const prefixKeys = new Set([...config?.stripPrefixKeys ?? [], ...paramNames]);
|
|
1069
1417
|
return (...args) => {
|
|
1070
1418
|
const firstArg = args[0] ?? {};
|
|
1071
|
-
|
|
1072
|
-
if (config?.pathPrefix) {
|
|
1073
|
-
pathPrefixStr = typeof config.pathPrefix === "function" ? config.pathPrefix(firstArg) : config.pathPrefix;
|
|
1074
|
-
if (pathPrefixStr && !pathPrefixStr.endsWith("/"))
|
|
1075
|
-
pathPrefixStr += "/";
|
|
1076
|
-
}
|
|
1077
|
-
let url = `${pathPrefixStr}${prefix}${endpoint.path}`;
|
|
1078
|
-
for (const name of paramNames) {
|
|
1079
|
-
const value = firstArg[name];
|
|
1080
|
-
if (value === undefined || value === null) {
|
|
1081
|
-
throw new Error(`Missing path param: ${name}`);
|
|
1082
|
-
}
|
|
1083
|
-
url = url.replace(`:${name}`, encodeURIComponent(String(value)));
|
|
1084
|
-
}
|
|
1085
|
-
if (url.endsWith("/"))
|
|
1086
|
-
url = url.slice(0, -1);
|
|
1419
|
+
const plan = planClientRequest(endpoint, prefix, firstArg, config);
|
|
1087
1420
|
if (endpoint.multipart) {
|
|
1088
1421
|
const file = firstArg[endpoint.multipart];
|
|
1089
1422
|
if (!isMultipartFile(file)) {
|
|
@@ -1094,59 +1427,28 @@ function createHttpMethod(endpoint, prefix, client, config) {
|
|
|
1094
1427
|
}
|
|
1095
1428
|
const formData = new FormData;
|
|
1096
1429
|
appendMultipartFile(formData, endpoint.multipart, file);
|
|
1097
|
-
appendFormFields(formData,
|
|
1098
|
-
return withOutput(endpoint, client[httpMethod](
|
|
1430
|
+
appendFormFields(formData, plan.remainingArgs, new Set([endpoint.multipart]));
|
|
1431
|
+
return withOutput(endpoint, client[httpMethod](plan.relativeUrl, formData, withTimeout(undefined, endpoint)));
|
|
1099
1432
|
}
|
|
1100
|
-
if (
|
|
1101
|
-
|
|
1102
|
-
return withOutput(endpoint, client.get(url, withTimeout(params ? { params } : undefined, endpoint)));
|
|
1433
|
+
if (httpMethod === "get") {
|
|
1434
|
+
return withOutput(endpoint, client.get(plan.relativeUrl, withTimeout(undefined, endpoint)));
|
|
1103
1435
|
}
|
|
1104
1436
|
if (httpMethod === "delete") {
|
|
1105
|
-
|
|
1106
|
-
return withOutput(endpoint, client.delete(url, withTimeout(params ? { params } : undefined, endpoint)));
|
|
1107
|
-
}
|
|
1108
|
-
const payload = {};
|
|
1109
|
-
for (const [key, value] of Object.entries(firstArg)) {
|
|
1110
|
-
if (!prefixKeys.has(key) && value !== undefined) {
|
|
1111
|
-
payload[key] = value;
|
|
1112
|
-
}
|
|
1437
|
+
return withOutput(endpoint, client.delete(plan.relativeUrl, withTimeout(undefined, endpoint)));
|
|
1113
1438
|
}
|
|
1114
|
-
return withOutput(endpoint, client[httpMethod](
|
|
1439
|
+
return withOutput(endpoint, client[httpMethod](plan.relativeUrl, Object.keys(plan.remainingArgs).length > 0 ? plan.remainingArgs : undefined, withTimeout(undefined, endpoint)));
|
|
1115
1440
|
};
|
|
1116
1441
|
}
|
|
1117
1442
|
function createFetchMethod(endpoint, prefix, config, contractConfig) {
|
|
1118
|
-
const prefixKeys = new Set(contractConfig?.stripPrefixKeys ?? []);
|
|
1119
1443
|
return async (args) => {
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
pathPrefixStr = typeof contractConfig.pathPrefix === "function" ? contractConfig.pathPrefix(args ?? {}) : contractConfig.pathPrefix;
|
|
1123
|
-
if (pathPrefixStr && !pathPrefixStr.endsWith("/"))
|
|
1124
|
-
pathPrefixStr += "/";
|
|
1125
|
-
}
|
|
1126
|
-
let url = buildFetchUrl(config.baseUrl, prefix, endpoint.path, args, pathPrefixStr);
|
|
1444
|
+
const plan = planClientRequest(endpoint, prefix, args ?? {}, contractConfig);
|
|
1445
|
+
const url = joinClientBaseUrl(config.baseUrl, plan.relativeUrl);
|
|
1127
1446
|
const headers = {
|
|
1128
1447
|
Accept: "application/json",
|
|
1129
1448
|
...typeof config.headers === "function" ? config.headers() : config.headers
|
|
1130
1449
|
};
|
|
1131
1450
|
const signal = endpoint.timeout !== undefined ? AbortSignal.timeout(endpoint.timeout) : undefined;
|
|
1132
|
-
const
|
|
1133
|
-
const hasBody = !isQuery && !endpoint.multipart && endpoint.input && args;
|
|
1134
|
-
if (isQuery && args) {
|
|
1135
|
-
const remaining = stripParams(args, endpoint.path, prefixKeys);
|
|
1136
|
-
const params = collectQueryParams(remaining, new Set, endpoint);
|
|
1137
|
-
if (params) {
|
|
1138
|
-
const searchParams = new URLSearchParams;
|
|
1139
|
-
for (const [k, v] of Object.entries(params)) {
|
|
1140
|
-
if (Array.isArray(v)) {
|
|
1141
|
-
for (const item of v)
|
|
1142
|
-
searchParams.append(k, String(item));
|
|
1143
|
-
} else {
|
|
1144
|
-
searchParams.set(k, String(v));
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
url += `?${searchParams}`;
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1451
|
+
const hasBody = endpoint.method !== "GET" && endpoint.method !== "DELETE" && !endpoint.multipart && endpoint.input && args;
|
|
1150
1452
|
if (hasBody)
|
|
1151
1453
|
headers["Content-Type"] = "application/json";
|
|
1152
1454
|
if (endpoint.multipart && args) {
|
|
@@ -1156,7 +1458,7 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
|
|
|
1156
1458
|
}
|
|
1157
1459
|
const formData = new FormData;
|
|
1158
1460
|
appendMultipartFile(formData, endpoint.multipart, file);
|
|
1159
|
-
appendFormFields(formData,
|
|
1461
|
+
appendFormFields(formData, plan.remainingArgs, new Set([endpoint.multipart]));
|
|
1160
1462
|
const res2 = await fetch(url, {
|
|
1161
1463
|
method: endpoint.method,
|
|
1162
1464
|
headers,
|
|
@@ -1180,7 +1482,7 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
|
|
|
1180
1482
|
credentials: config.credentials,
|
|
1181
1483
|
signal,
|
|
1182
1484
|
...hasBody && {
|
|
1183
|
-
body: JSON.stringify(
|
|
1485
|
+
body: JSON.stringify(plan.remainingArgs)
|
|
1184
1486
|
}
|
|
1185
1487
|
});
|
|
1186
1488
|
if (!res.ok) {
|
|
@@ -1203,36 +1505,6 @@ async function throwForErrorResponse(res, config, fallbackBody) {
|
|
|
1203
1505
|
}
|
|
1204
1506
|
throw new ApiError("HTTP_ERROR", res.status, { body });
|
|
1205
1507
|
}
|
|
1206
|
-
function extractParamNames(path) {
|
|
1207
|
-
const matches = path.match(/:(\w+)/g);
|
|
1208
|
-
return matches ? matches.map((m) => m.slice(1)) : [];
|
|
1209
|
-
}
|
|
1210
|
-
function buildFetchUrl(baseUrl, prefix, path, args, pathPrefix = "") {
|
|
1211
|
-
let fullPath = `/${pathPrefix}${prefix}${path === "/" ? "" : path}`;
|
|
1212
|
-
if (args) {
|
|
1213
|
-
fullPath = fullPath.replace(/:(\w+)/g, (_, key) => {
|
|
1214
|
-
const val = args[key];
|
|
1215
|
-
if (val === undefined || val === null) {
|
|
1216
|
-
throw new Error(`Missing path param: ${key}`);
|
|
1217
|
-
}
|
|
1218
|
-
return encodeURIComponent(String(val));
|
|
1219
|
-
});
|
|
1220
|
-
}
|
|
1221
|
-
return `${baseUrl}${fullPath}`;
|
|
1222
|
-
}
|
|
1223
|
-
function stripParams(args, path, extra) {
|
|
1224
|
-
const skip = new Set(extra);
|
|
1225
|
-
for (const match of path.matchAll(/:(\w+)/g)) {
|
|
1226
|
-
if (match[1])
|
|
1227
|
-
skip.add(match[1]);
|
|
1228
|
-
}
|
|
1229
|
-
const result = {};
|
|
1230
|
-
for (const [k, v] of Object.entries(args)) {
|
|
1231
|
-
if (!skip.has(k))
|
|
1232
|
-
result[k] = v;
|
|
1233
|
-
}
|
|
1234
|
-
return result;
|
|
1235
|
-
}
|
|
1236
1508
|
|
|
1237
1509
|
// src/tools/remote.ts
|
|
1238
1510
|
var HTTP_ONLY = Object.freeze(["HTTP"]);
|
|
@@ -1255,7 +1527,7 @@ function implementRemote(contract, http, options) {
|
|
|
1255
1527
|
serviceName: contract.meta.prefix,
|
|
1256
1528
|
key,
|
|
1257
1529
|
toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
|
|
1258
|
-
expose: endpoint.rawResponse ? HTTP_ONLY : endpoint.expose,
|
|
1530
|
+
expose: endpoint.rawResponse || endpoint.rawBody || endpoint.responseMeta ? HTTP_ONLY : endpoint.expose,
|
|
1259
1531
|
ui: "ui" in endpoint ? endpoint.ui : undefined,
|
|
1260
1532
|
annotations: "annotations" in endpoint ? endpoint.annotations : undefined,
|
|
1261
1533
|
meta: mergeMeta(contract.meta.meta, endpoint.meta),
|
|
@@ -1264,8 +1536,11 @@ function implementRemote(contract, http, options) {
|
|
|
1264
1536
|
inputSchema: endpoint.input,
|
|
1265
1537
|
outputSchema: endpoint.output,
|
|
1266
1538
|
multipart: endpoint.multipart,
|
|
1539
|
+
maxJsonBodyBytes: endpoint.maxJsonBodyBytes,
|
|
1267
1540
|
idempotent: endpoint.idempotent,
|
|
1268
1541
|
rawResponse: endpoint.rawResponse,
|
|
1542
|
+
rawBody: endpoint.rawBody,
|
|
1543
|
+
responseMeta: endpoint.responseMeta,
|
|
1269
1544
|
contentType: "contentType" in endpoint ? endpoint.contentType : undefined,
|
|
1270
1545
|
handler: async (ctx) => {
|
|
1271
1546
|
const call = client[key];
|
|
@@ -1296,7 +1571,7 @@ function implementRemote(contract, http, options) {
|
|
|
1296
1571
|
function createToolLogger(config = {}) {
|
|
1297
1572
|
const log = config.log ?? ((line) => console.info(line));
|
|
1298
1573
|
return {
|
|
1299
|
-
afterToolCall: (toolName,
|
|
1574
|
+
afterToolCall: ({ toolName, result, durationMs, context, endpoint }) => {
|
|
1300
1575
|
const record = {
|
|
1301
1576
|
tool: toolName,
|
|
1302
1577
|
service: endpoint.serviceName,
|
|
@@ -1350,7 +1625,7 @@ function summarizeTransports(services) {
|
|
|
1350
1625
|
// src/tools/view-file.ts
|
|
1351
1626
|
import { readFile, realpath, stat } from "node:fs/promises";
|
|
1352
1627
|
import { extname as extname2, resolve } from "node:path";
|
|
1353
|
-
import { z as
|
|
1628
|
+
import { z as z4 } from "zod";
|
|
1354
1629
|
var MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
1355
1630
|
var EXT_MIME = {
|
|
1356
1631
|
".png": "image/png",
|
|
@@ -1450,7 +1725,7 @@ function mountViewFile(server, options = {}) {
|
|
|
1450
1725
|
server.registerTool("view_file", {
|
|
1451
1726
|
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
1727
|
inputSchema: {
|
|
1453
|
-
paths:
|
|
1728
|
+
paths: z4.union([z4.string(), z4.array(z4.string())]).describe("Media URL(s) or file path(s) to view")
|
|
1454
1729
|
},
|
|
1455
1730
|
annotations: { title: "View Media", readOnlyHint: true, idempotentHint: true }
|
|
1456
1731
|
}, async (args) => {
|
|
@@ -1487,9 +1762,9 @@ export {
|
|
|
1487
1762
|
listToolNames,
|
|
1488
1763
|
inlineMcpAppBundle,
|
|
1489
1764
|
implementRemote,
|
|
1490
|
-
|
|
1491
|
-
flattenDiscriminatedUnion,
|
|
1765
|
+
flattenToolJsonSchema,
|
|
1492
1766
|
findUntypedProperties,
|
|
1767
|
+
findNonPortableFormats,
|
|
1493
1768
|
createToolkit,
|
|
1494
1769
|
createToolLogger,
|
|
1495
1770
|
createStdioMcpServer,
|
|
@@ -1501,5 +1776,6 @@ export {
|
|
|
1501
1776
|
buildMcpServer,
|
|
1502
1777
|
RESOURCE_MIME_TYPE,
|
|
1503
1778
|
PROTECTED_RESOURCE_PATH,
|
|
1779
|
+
PORTABLE_JSON_SCHEMA_FORMATS,
|
|
1504
1780
|
EXT_APPS_BUNDLE_PLACEHOLDER
|
|
1505
1781
|
};
|