stitchkit 0.37.0 → 0.39.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 +1 -1
- package/dist/browser/client-url.d.ts +13 -0
- package/dist/browser/client-url.d.ts.map +1 -0
- package/dist/browser/client.d.ts +41 -5
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +10 -7
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/contract/define.d.ts +102 -4
- package/dist/contract/define.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-czmqks7r.js → index-6jypn22c.js} +1 -1
- package/dist/{index-4pvtq6h2.js → index-7rbzbnnf.js} +104 -30
- package/dist/{index-xq45akyd.js → index-92gs1m5b.js} +1 -1
- package/dist/{index-e497hxcy.js → index-fyfk537k.js} +50 -5
- package/dist/{index-g3jrbd0z.js → index-xax049k6.js} +101 -5
- package/dist/{index-s6yhmg1k.js → index-y5scd1cr.js} +3 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +243 -111
- package/dist/internal/route-pattern.d.ts +7 -0
- package/dist/internal/route-pattern.d.ts.map +1 -0
- package/dist/node.js +3 -3
- package/dist/observability/index.js +1 -1
- package/dist/react/entity-cache.d.ts +56 -42
- package/dist/react/entity-cache.d.ts.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +98 -46
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.js +22 -7
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/openapi.d.ts.map +1 -1
- 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 +2 -0
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/socket-io.d.ts +1 -1
- package/dist/server/types.d.ts +14 -8
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/agent.d.ts +3 -0
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/invoker.d.ts +33 -0
- package/dist/tools/invoker.d.ts.map +1 -0
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/names.d.ts +1 -1
- package/dist/tools/names.d.ts.map +1 -1
- package/dist/tools/native-mcp.d.ts +6 -33
- package/dist/tools/native-mcp.d.ts.map +1 -1
- package/dist/tools/remote.d.ts.map +1 -1
- package/dist/tools/runtime-tool.d.ts +58 -0
- package/dist/tools/runtime-tool.d.ts.map +1 -0
- package/dist/tools.d.ts +3 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +295 -182
- package/llms-full.txt +562 -79
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
inputIsQuery,
|
|
3
3
|
signJwt,
|
|
4
4
|
verifyPkce
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-92gs1m5b.js";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_CORS_ALLOW_HEADERS
|
|
8
|
-
} from "./index-
|
|
8
|
+
} from "./index-6jypn22c.js";
|
|
9
9
|
import {
|
|
10
10
|
assertToolName,
|
|
11
11
|
assertUniqueToolName,
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
readCapped,
|
|
25
25
|
toolResultFromError,
|
|
26
26
|
writeDownload
|
|
27
|
-
} from "./index-
|
|
27
|
+
} from "./index-y5scd1cr.js";
|
|
28
28
|
import {
|
|
29
29
|
toJsonSchema
|
|
30
30
|
} from "./index-frfyw9fa.js";
|
|
@@ -33,15 +33,63 @@ import {
|
|
|
33
33
|
} from "./index-x3fcszf8.js";
|
|
34
34
|
import {
|
|
35
35
|
AppError,
|
|
36
|
-
formatZodError,
|
|
37
36
|
getTraceId,
|
|
38
37
|
isRecord,
|
|
39
38
|
mergeMeta,
|
|
39
|
+
parseTrailingWildcard,
|
|
40
40
|
typedEntries
|
|
41
|
-
} from "./index-
|
|
41
|
+
} from "./index-fyfk537k.js";
|
|
42
42
|
|
|
43
43
|
// src/tools/agent.ts
|
|
44
44
|
import { jsonSchema, tool } from "ai";
|
|
45
|
+
|
|
46
|
+
// src/tools/runtime-tool.ts
|
|
47
|
+
function defineRuntimeTool(definition) {
|
|
48
|
+
if (definition.transports?.length === 0) {
|
|
49
|
+
throw new Error(`Runtime tool "${definition.name}" must expose at least one transport`);
|
|
50
|
+
}
|
|
51
|
+
return definition;
|
|
52
|
+
}
|
|
53
|
+
function runtimeToolSupports(definition, transport) {
|
|
54
|
+
if (definition.transports?.length === 0) {
|
|
55
|
+
throw new Error(`Runtime tool "${definition.name}" must expose at least one transport`);
|
|
56
|
+
}
|
|
57
|
+
return !definition.transports || definition.transports.includes(transport);
|
|
58
|
+
}
|
|
59
|
+
function runtimeToolIdentity(definition) {
|
|
60
|
+
return {
|
|
61
|
+
method: definition.identity.method,
|
|
62
|
+
desc: definition.description,
|
|
63
|
+
serviceName: definition.identity.serviceName,
|
|
64
|
+
key: definition.identity.action,
|
|
65
|
+
toolName: definition.name,
|
|
66
|
+
scope: definition.identity.scope,
|
|
67
|
+
meta: definition.identity.meta,
|
|
68
|
+
annotations: definition.annotations,
|
|
69
|
+
ui: definition.ui
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function runtimeToolMountable(definition) {
|
|
73
|
+
assertToolName(definition.name, definition.identity.serviceName, definition.identity.action);
|
|
74
|
+
const method = {
|
|
75
|
+
...runtimeToolIdentity(definition),
|
|
76
|
+
inputSchema: definition.input,
|
|
77
|
+
outputSchema: definition.output,
|
|
78
|
+
handler: definition.handler
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
method,
|
|
82
|
+
name: definition.name,
|
|
83
|
+
argumentSchema: definition.input,
|
|
84
|
+
presentationSchema: buildToolPresentationSchema({
|
|
85
|
+
inputSchema: definition.input,
|
|
86
|
+
unrepresentable: "any"
|
|
87
|
+
}),
|
|
88
|
+
shouldExtend: false
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/tools/agent.ts
|
|
45
93
|
function mountAgent(services, config = {}) {
|
|
46
94
|
const serviceList = Array.isArray(services) ? services : [services];
|
|
47
95
|
const tools = {};
|
|
@@ -80,8 +128,88 @@ function mountAgent(services, config = {}) {
|
|
|
80
128
|
});
|
|
81
129
|
}
|
|
82
130
|
}
|
|
131
|
+
for (const definition of config.runtimeTools ?? []) {
|
|
132
|
+
if (!runtimeToolSupports(definition, "AGENT"))
|
|
133
|
+
continue;
|
|
134
|
+
const mountable = runtimeToolMountable(definition);
|
|
135
|
+
assertUniqueToolName(definition.name, Object.hasOwn(tools, definition.name), "agent tool name");
|
|
136
|
+
const inputSchema = jsonSchema(mountable.presentationSchema, {
|
|
137
|
+
validate: async (value) => isRecord(value) ? { success: true, value } : { success: false, error: new Error("Tool arguments must be an object") }
|
|
138
|
+
});
|
|
139
|
+
const execute = async (rawArgs) => {
|
|
140
|
+
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
141
|
+
try {
|
|
142
|
+
const result = await runTool(mountable, args);
|
|
143
|
+
if (result.ok)
|
|
144
|
+
return result.data;
|
|
145
|
+
return formatToolError(result, mountable.name, config.errorHint);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return formatToolError(toolResultFromError(err), mountable.name, config.errorHint);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
if (definition.present?.agent) {
|
|
151
|
+
const presenter = definition.present.agent;
|
|
152
|
+
const output = definition.output;
|
|
153
|
+
tools[definition.name] = tool({
|
|
154
|
+
description: definition.description,
|
|
155
|
+
inputSchema,
|
|
156
|
+
outputSchema: output,
|
|
157
|
+
execute,
|
|
158
|
+
toModelOutput: async ({ output: rawOutput }) => {
|
|
159
|
+
const parsed = output.safeParse(rawOutput);
|
|
160
|
+
if (!parsed.success) {
|
|
161
|
+
return { type: "text", value: JSON.stringify(rawOutput) };
|
|
162
|
+
}
|
|
163
|
+
return presenter(parsed.data);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
tools[definition.name] = tool({
|
|
168
|
+
description: definition.description,
|
|
169
|
+
inputSchema,
|
|
170
|
+
...definition.output && { outputSchema: definition.output },
|
|
171
|
+
execute
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
83
175
|
return tools;
|
|
84
176
|
}
|
|
177
|
+
// src/tools/invoker.ts
|
|
178
|
+
function createToolInvoker(services, config) {
|
|
179
|
+
const serviceList = Array.isArray(services) ? services : [services];
|
|
180
|
+
const tools = new Map;
|
|
181
|
+
for (const service of serviceList) {
|
|
182
|
+
for (const tool2 of collectTools(service, config.transport, {
|
|
183
|
+
extend: config.extend,
|
|
184
|
+
flattenUnionInput: config.flattenUnionInput
|
|
185
|
+
})) {
|
|
186
|
+
assertUniqueToolName(tool2.name, tools.has(tool2.name), "in-process tool name");
|
|
187
|
+
tools.set(tool2.name, tool2);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const names = Object.freeze([...tools.keys()].sort());
|
|
191
|
+
const runTool = createToolRunner({
|
|
192
|
+
source: config.source ?? "internal",
|
|
193
|
+
context: config.context,
|
|
194
|
+
hooks: config.hooks,
|
|
195
|
+
lifecycle: config.lifecycle,
|
|
196
|
+
extend: config.extend,
|
|
197
|
+
coerceJsonArgs: config.coerceJsonArgs,
|
|
198
|
+
onOutputStrip: config.onOutputStrip
|
|
199
|
+
});
|
|
200
|
+
return Object.freeze({
|
|
201
|
+
names,
|
|
202
|
+
invoke(name, args) {
|
|
203
|
+
const tool2 = tools.get(name);
|
|
204
|
+
if (!tool2) {
|
|
205
|
+
throw new AppError("NOT_FOUND", `Unknown tool: ${name}`, 404, {
|
|
206
|
+
available: names
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return runTool(tool2, args);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
85
213
|
// src/tools/list-names.ts
|
|
86
214
|
var TOOL_TRANSPORTS = ["MCP", "AGENT", "CLI"];
|
|
87
215
|
function listToolNames(services) {
|
|
@@ -142,72 +270,21 @@ function inlineMcpAppBundle(html) {
|
|
|
142
270
|
// src/tools/native-mcp.ts
|
|
143
271
|
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
144
272
|
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
273
|
function isCallToolResult(value) {
|
|
159
274
|
return CallToolResultSchema.safeParse(value).success;
|
|
160
275
|
}
|
|
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
276
|
function createNativeMcpRegistrar(server, config) {
|
|
180
277
|
return {
|
|
181
278
|
rawServer: server,
|
|
182
279
|
registerTool: (definition) => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
};
|
|
280
|
+
if (!runtimeToolSupports(definition, "MCP"))
|
|
281
|
+
return;
|
|
282
|
+
assertUniqueToolName(definition.name, config.takenNames.has(definition.name), "MCP tool name");
|
|
283
|
+
config.takenNames.add(definition.name);
|
|
284
|
+
const mountable = runtimeToolMountable(definition);
|
|
202
285
|
const [prepared] = config.prepare(mountable);
|
|
203
286
|
if (!prepared)
|
|
204
287
|
return;
|
|
205
|
-
const executionOperation = {
|
|
206
|
-
...identity,
|
|
207
|
-
inputSchema: definition.input,
|
|
208
|
-
outputSchema: nativeResultSchema(definition.output),
|
|
209
|
-
handler: definition.handler
|
|
210
|
-
};
|
|
211
288
|
const toolConfig = {
|
|
212
289
|
description: definition.description,
|
|
213
290
|
inputSchema: z.looseObject({}).meta(presentationMetadata(prepared.inputSchema))
|
|
@@ -224,16 +301,32 @@ function createNativeMcpRegistrar(server, config) {
|
|
|
224
301
|
}
|
|
225
302
|
server.registerTool(definition.name, toolConfig, async (rawArgs) => {
|
|
226
303
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
227
|
-
const result = await executeToolMethod(
|
|
228
|
-
if (!result.ok)
|
|
229
|
-
return config.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
304
|
+
const result = await executeToolMethod(mountable.method, definition.name, args, { ...config.context, source: "mcp" }, config.hooks, config.lifecycle, config.coerceJsonArgs ?? true, config.onOutputStrip ? (paths) => config.onOutputStrip?.(definition.name, paths) : undefined);
|
|
305
|
+
if (!result.ok || !definition.present?.mcp) {
|
|
306
|
+
return config.formatResult(result, prepared.outputMode, definition.name);
|
|
307
|
+
}
|
|
308
|
+
const presented = await definition.present.mcp(result.data);
|
|
309
|
+
if ("structuredContent" in presented || "isError" in presented) {
|
|
310
|
+
return config.formatResult({
|
|
311
|
+
ok: false,
|
|
312
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
313
|
+
details: {
|
|
314
|
+
message: "Runtime MCP presenter cannot set framework-owned structuredContent or isError"
|
|
315
|
+
}
|
|
316
|
+
}, "none", definition.name);
|
|
317
|
+
}
|
|
318
|
+
const structured = config.formatResult(result, prepared.outputMode, definition.name).structuredContent;
|
|
319
|
+
const response = {
|
|
320
|
+
...presented,
|
|
321
|
+
...structured && { structuredContent: structured }
|
|
322
|
+
};
|
|
323
|
+
if (isCallToolResult(response))
|
|
324
|
+
return response;
|
|
325
|
+
return config.formatResult({
|
|
233
326
|
ok: false,
|
|
234
327
|
code: "INTERNAL_SERVER_ERROR",
|
|
235
|
-
details: { message: "
|
|
236
|
-
}, definition.name);
|
|
328
|
+
details: { message: "Runtime MCP presenter returned an invalid CallToolResult" }
|
|
329
|
+
}, "none", definition.name);
|
|
237
330
|
});
|
|
238
331
|
}
|
|
239
332
|
};
|
|
@@ -606,17 +699,19 @@ function buildMcpServerFromPrepared(config, auth, prepared) {
|
|
|
606
699
|
onOutputStrip: config.onOutputStrip
|
|
607
700
|
});
|
|
608
701
|
if (config.nativeTools) {
|
|
702
|
+
const takenNames = new Set(prepared.map((tool2) => tool2.mountable.name));
|
|
609
703
|
const registrar = createNativeMcpRegistrar(server, {
|
|
610
704
|
context,
|
|
611
705
|
hooks: config.hooks,
|
|
612
706
|
lifecycle: config.lifecycle,
|
|
613
707
|
coerceJsonArgs: config.coerceJsonArgs,
|
|
614
708
|
onOutputStrip: config.onOutputStrip,
|
|
709
|
+
takenNames,
|
|
615
710
|
prepare: (tool2) => prepareMcpTools([tool2], {
|
|
616
711
|
schemaValidation: config.schemaValidation,
|
|
617
712
|
logger: config.logger
|
|
618
713
|
}),
|
|
619
|
-
|
|
714
|
+
formatResult: (result, mode, toolName) => formatMcpResult(result, mode, toolName, config.errorHint)
|
|
620
715
|
});
|
|
621
716
|
config.nativeTools(registrar, auth);
|
|
622
717
|
}
|
|
@@ -1235,6 +1330,119 @@ function appendFormFields(formData, values, skipKeys) {
|
|
|
1235
1330
|
}
|
|
1236
1331
|
}
|
|
1237
1332
|
|
|
1333
|
+
// src/browser/client-url.ts
|
|
1334
|
+
function isParamArray(value) {
|
|
1335
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string" || typeof item === "number");
|
|
1336
|
+
}
|
|
1337
|
+
function collectQueryParams(args, endpoint) {
|
|
1338
|
+
const params = {};
|
|
1339
|
+
let hasParams = false;
|
|
1340
|
+
for (const [key, value] of Object.entries(args)) {
|
|
1341
|
+
if (value === undefined || value === null)
|
|
1342
|
+
continue;
|
|
1343
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isParamArray(value)) {
|
|
1344
|
+
params[key] = value;
|
|
1345
|
+
hasParams = true;
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
const what = Array.isArray(value) ? "an array with non-primitive items" : typeof value === "object" ? "a nested object" : `a ${typeof value}`;
|
|
1349
|
+
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).");
|
|
1350
|
+
}
|
|
1351
|
+
return hasParams ? params : undefined;
|
|
1352
|
+
}
|
|
1353
|
+
function hasStringKeys(args, keys) {
|
|
1354
|
+
for (const key of keys) {
|
|
1355
|
+
if (typeof args[key] !== "string")
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
return true;
|
|
1359
|
+
}
|
|
1360
|
+
function resolvePathPrefix(config, args) {
|
|
1361
|
+
if (!config?.pathPrefix)
|
|
1362
|
+
return "";
|
|
1363
|
+
if (typeof config.pathPrefix === "string")
|
|
1364
|
+
return config.pathPrefix;
|
|
1365
|
+
const keys = config.stripPrefixKeys ?? [];
|
|
1366
|
+
if (!hasStringKeys(args, keys)) {
|
|
1367
|
+
const missing = keys.find((key) => typeof args[key] !== "string");
|
|
1368
|
+
throw new Error(`Missing path prefix key: ${missing}`);
|
|
1369
|
+
}
|
|
1370
|
+
return config.pathPrefix(args);
|
|
1371
|
+
}
|
|
1372
|
+
function extractParamNames(path) {
|
|
1373
|
+
const matches = path.match(/:(\w+)/g);
|
|
1374
|
+
const names = matches ? matches.map((match) => match.slice(1)) : [];
|
|
1375
|
+
const wildcard = parseTrailingWildcard(path);
|
|
1376
|
+
if (wildcard)
|
|
1377
|
+
names.push(wildcard.name);
|
|
1378
|
+
return names;
|
|
1379
|
+
}
|
|
1380
|
+
function fillPathParams(path, args) {
|
|
1381
|
+
const wildcard = parseTrailingWildcard(path);
|
|
1382
|
+
let filled = path.replace(/:(\w+)/g, (_, key) => {
|
|
1383
|
+
const value = args[key];
|
|
1384
|
+
if (value === undefined || value === null) {
|
|
1385
|
+
throw new Error(`Missing path param: ${key}`);
|
|
1386
|
+
}
|
|
1387
|
+
return encodeURIComponent(String(value));
|
|
1388
|
+
});
|
|
1389
|
+
if (!wildcard)
|
|
1390
|
+
return filled;
|
|
1391
|
+
const wildcardValue = args[wildcard.name];
|
|
1392
|
+
if (wildcardValue === undefined || wildcardValue === null) {
|
|
1393
|
+
throw new Error(`Missing path param: ${wildcard.name}`);
|
|
1394
|
+
}
|
|
1395
|
+
const remainder = String(wildcardValue).split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
1396
|
+
filled = `${filled.slice(0, -(wildcard.name.length + 1))}${remainder}`;
|
|
1397
|
+
return filled;
|
|
1398
|
+
}
|
|
1399
|
+
function stripConsumedArgs(args, path, scopeKeys) {
|
|
1400
|
+
const consumed = new Set(scopeKeys);
|
|
1401
|
+
for (const name of extractParamNames(path))
|
|
1402
|
+
consumed.add(name);
|
|
1403
|
+
const remaining = {};
|
|
1404
|
+
for (const [key, value] of Object.entries(args)) {
|
|
1405
|
+
if (!consumed.has(key) && value !== undefined)
|
|
1406
|
+
remaining[key] = value;
|
|
1407
|
+
}
|
|
1408
|
+
return remaining;
|
|
1409
|
+
}
|
|
1410
|
+
function appendQuery(relativeUrl, params) {
|
|
1411
|
+
if (!params)
|
|
1412
|
+
return relativeUrl;
|
|
1413
|
+
const search = new URLSearchParams;
|
|
1414
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1415
|
+
if (Array.isArray(value)) {
|
|
1416
|
+
for (const item of value)
|
|
1417
|
+
search.append(key, String(item));
|
|
1418
|
+
} else {
|
|
1419
|
+
search.set(key, String(value));
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return search.size > 0 ? `${relativeUrl}?${search}` : relativeUrl;
|
|
1423
|
+
}
|
|
1424
|
+
function planClientRequest(endpoint, contractPrefix, args, config) {
|
|
1425
|
+
let pathPrefix = resolvePathPrefix(config, args);
|
|
1426
|
+
if (pathPrefix && !pathPrefix.endsWith("/"))
|
|
1427
|
+
pathPrefix += "/";
|
|
1428
|
+
if (pathPrefix.startsWith("/"))
|
|
1429
|
+
pathPrefix = pathPrefix.slice(1);
|
|
1430
|
+
const endpointPath = endpoint.path === "/" ? "" : endpoint.path;
|
|
1431
|
+
let relativeUrl = fillPathParams(`${pathPrefix}${contractPrefix}${endpointPath}`, args);
|
|
1432
|
+
if (relativeUrl.endsWith("/"))
|
|
1433
|
+
relativeUrl = relativeUrl.slice(0, -1);
|
|
1434
|
+
const remainingArgs = stripConsumedArgs(args, endpoint.path, config?.stripPrefixKeys ?? []);
|
|
1435
|
+
if (inputIsQuery(endpoint.method)) {
|
|
1436
|
+
relativeUrl = appendQuery(relativeUrl, collectQueryParams(remainingArgs, endpoint));
|
|
1437
|
+
}
|
|
1438
|
+
return { relativeUrl, remainingArgs };
|
|
1439
|
+
}
|
|
1440
|
+
function joinClientBaseUrl(baseUrl, relativeUrl) {
|
|
1441
|
+
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
1442
|
+
const path = relativeUrl.startsWith("/") ? relativeUrl : `/${relativeUrl}`;
|
|
1443
|
+
return `${base}${path}`;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1238
1446
|
// src/browser/http.ts
|
|
1239
1447
|
import ky, { isHTTPError } from "ky";
|
|
1240
1448
|
class ApiError extends Error {
|
|
@@ -1285,25 +1493,6 @@ function withOutput(endpoint, result) {
|
|
|
1285
1493
|
return result;
|
|
1286
1494
|
return result.then((value) => value === undefined ? undefined : schema.parse(value));
|
|
1287
1495
|
}
|
|
1288
|
-
function isParamArray(value) {
|
|
1289
|
-
return Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number");
|
|
1290
|
-
}
|
|
1291
|
-
function collectQueryParams(args, skipKeys, endpoint) {
|
|
1292
|
-
const params = {};
|
|
1293
|
-
let hasParams = false;
|
|
1294
|
-
for (const [key, value] of Object.entries(args)) {
|
|
1295
|
-
if (skipKeys.has(key) || value === undefined || value === null)
|
|
1296
|
-
continue;
|
|
1297
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isParamArray(value)) {
|
|
1298
|
-
params[key] = value;
|
|
1299
|
-
hasParams = true;
|
|
1300
|
-
continue;
|
|
1301
|
-
}
|
|
1302
|
-
const what = Array.isArray(value) ? "an array with non-primitive items" : typeof value === "object" ? "a nested object" : `a ${typeof value}`;
|
|
1303
|
-
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).");
|
|
1304
|
-
}
|
|
1305
|
-
return hasParams ? params : undefined;
|
|
1306
|
-
}
|
|
1307
1496
|
function createClient(contract, configOrClient, contractConfig) {
|
|
1308
1497
|
const client = {};
|
|
1309
1498
|
const makeMethod = isHttpAdapter(configOrClient) ? (endpoint) => createHttpMethod(endpoint, contract.meta.prefix, configOrClient, contractConfig) : (endpoint) => createFetchMethod(endpoint, contract.meta.prefix, configOrClient, contractConfig);
|
|
@@ -1322,90 +1511,41 @@ function setClientMethod(target, key, method) {
|
|
|
1322
1511
|
}
|
|
1323
1512
|
function createHttpMethod(endpoint, prefix, client, config) {
|
|
1324
1513
|
const httpMethod = endpoint.method.toLowerCase();
|
|
1325
|
-
const isGet = httpMethod === "get";
|
|
1326
|
-
const paramNames = extractParamNames(endpoint.path);
|
|
1327
|
-
const prefixKeys = new Set([...config?.stripPrefixKeys ?? [], ...paramNames]);
|
|
1328
1514
|
return (...args) => {
|
|
1329
1515
|
const firstArg = args[0] ?? {};
|
|
1330
|
-
|
|
1331
|
-
if (config?.pathPrefix) {
|
|
1332
|
-
pathPrefixStr = typeof config.pathPrefix === "function" ? config.pathPrefix(firstArg) : config.pathPrefix;
|
|
1333
|
-
if (pathPrefixStr && !pathPrefixStr.endsWith("/"))
|
|
1334
|
-
pathPrefixStr += "/";
|
|
1335
|
-
}
|
|
1336
|
-
let url = `${pathPrefixStr}${prefix}${endpoint.path}`;
|
|
1337
|
-
for (const name of paramNames) {
|
|
1338
|
-
const value = firstArg[name];
|
|
1339
|
-
if (value === undefined || value === null) {
|
|
1340
|
-
throw new Error(`Missing path param: ${name}`);
|
|
1341
|
-
}
|
|
1342
|
-
url = url.replace(`:${name}`, encodeURIComponent(String(value)));
|
|
1343
|
-
}
|
|
1344
|
-
if (url.endsWith("/"))
|
|
1345
|
-
url = url.slice(0, -1);
|
|
1516
|
+
const plan = planClientRequest(endpoint, prefix, firstArg, config);
|
|
1346
1517
|
if (endpoint.multipart) {
|
|
1347
1518
|
const file = firstArg[endpoint.multipart];
|
|
1348
1519
|
if (!isMultipartFile(file)) {
|
|
1349
1520
|
throw new Error(`Missing multipart file field: ${endpoint.multipart}`);
|
|
1350
1521
|
}
|
|
1351
|
-
if (httpMethod === "get" || httpMethod === "delete") {
|
|
1522
|
+
if (httpMethod === "get" || httpMethod === "head" || httpMethod === "delete") {
|
|
1352
1523
|
throw new Error(`Multipart endpoint ${endpoint.method} ${endpoint.path} must be POST / PUT / PATCH`);
|
|
1353
1524
|
}
|
|
1354
1525
|
const formData = new FormData;
|
|
1355
1526
|
appendMultipartFile(formData, endpoint.multipart, file);
|
|
1356
|
-
appendFormFields(formData,
|
|
1357
|
-
return withOutput(endpoint, client[httpMethod](
|
|
1527
|
+
appendFormFields(formData, plan.remainingArgs, new Set([endpoint.multipart]));
|
|
1528
|
+
return withOutput(endpoint, client[httpMethod](plan.relativeUrl, formData, withTimeout(undefined, endpoint)));
|
|
1358
1529
|
}
|
|
1359
|
-
if (
|
|
1360
|
-
|
|
1361
|
-
return withOutput(endpoint, client.get(url, withTimeout(params ? { params } : undefined, endpoint)));
|
|
1530
|
+
if (httpMethod === "get" || httpMethod === "head") {
|
|
1531
|
+
return withOutput(endpoint, client[httpMethod](plan.relativeUrl, withTimeout(undefined, endpoint)));
|
|
1362
1532
|
}
|
|
1363
1533
|
if (httpMethod === "delete") {
|
|
1364
|
-
|
|
1365
|
-
return withOutput(endpoint, client.delete(url, withTimeout(params ? { params } : undefined, endpoint)));
|
|
1366
|
-
}
|
|
1367
|
-
const payload = {};
|
|
1368
|
-
for (const [key, value] of Object.entries(firstArg)) {
|
|
1369
|
-
if (!prefixKeys.has(key) && value !== undefined) {
|
|
1370
|
-
payload[key] = value;
|
|
1371
|
-
}
|
|
1534
|
+
return withOutput(endpoint, client.delete(plan.relativeUrl, withTimeout(undefined, endpoint)));
|
|
1372
1535
|
}
|
|
1373
|
-
return withOutput(endpoint, client[httpMethod](
|
|
1536
|
+
return withOutput(endpoint, client[httpMethod](plan.relativeUrl, Object.keys(plan.remainingArgs).length > 0 ? plan.remainingArgs : undefined, withTimeout(undefined, endpoint)));
|
|
1374
1537
|
};
|
|
1375
1538
|
}
|
|
1376
1539
|
function createFetchMethod(endpoint, prefix, config, contractConfig) {
|
|
1377
|
-
const prefixKeys = new Set(contractConfig?.stripPrefixKeys ?? []);
|
|
1378
1540
|
return async (args) => {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
pathPrefixStr = typeof contractConfig.pathPrefix === "function" ? contractConfig.pathPrefix(args ?? {}) : contractConfig.pathPrefix;
|
|
1382
|
-
if (pathPrefixStr && !pathPrefixStr.endsWith("/"))
|
|
1383
|
-
pathPrefixStr += "/";
|
|
1384
|
-
}
|
|
1385
|
-
let url = buildFetchUrl(config.baseUrl, prefix, endpoint.path, args, pathPrefixStr);
|
|
1541
|
+
const plan = planClientRequest(endpoint, prefix, args ?? {}, contractConfig);
|
|
1542
|
+
const url = joinClientBaseUrl(config.baseUrl, plan.relativeUrl);
|
|
1386
1543
|
const headers = {
|
|
1387
1544
|
Accept: "application/json",
|
|
1388
1545
|
...typeof config.headers === "function" ? config.headers() : config.headers
|
|
1389
1546
|
};
|
|
1390
1547
|
const signal = endpoint.timeout !== undefined ? AbortSignal.timeout(endpoint.timeout) : undefined;
|
|
1391
|
-
const
|
|
1392
|
-
const hasBody = !isQuery && !endpoint.multipart && endpoint.input && args;
|
|
1393
|
-
if (isQuery && args) {
|
|
1394
|
-
const remaining = stripParams(args, endpoint.path, prefixKeys);
|
|
1395
|
-
const params = collectQueryParams(remaining, new Set, endpoint);
|
|
1396
|
-
if (params) {
|
|
1397
|
-
const searchParams = new URLSearchParams;
|
|
1398
|
-
for (const [k, v] of Object.entries(params)) {
|
|
1399
|
-
if (Array.isArray(v)) {
|
|
1400
|
-
for (const item of v)
|
|
1401
|
-
searchParams.append(k, String(item));
|
|
1402
|
-
} else {
|
|
1403
|
-
searchParams.set(k, String(v));
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
url += `?${searchParams}`;
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1548
|
+
const hasBody = endpoint.method !== "GET" && endpoint.method !== "HEAD" && endpoint.method !== "DELETE" && !endpoint.multipart && endpoint.input && args;
|
|
1409
1549
|
if (hasBody)
|
|
1410
1550
|
headers["Content-Type"] = "application/json";
|
|
1411
1551
|
if (endpoint.multipart && args) {
|
|
@@ -1415,7 +1555,7 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
|
|
|
1415
1555
|
}
|
|
1416
1556
|
const formData = new FormData;
|
|
1417
1557
|
appendMultipartFile(formData, endpoint.multipart, file);
|
|
1418
|
-
appendFormFields(formData,
|
|
1558
|
+
appendFormFields(formData, plan.remainingArgs, new Set([endpoint.multipart]));
|
|
1419
1559
|
const res2 = await fetch(url, {
|
|
1420
1560
|
method: endpoint.method,
|
|
1421
1561
|
headers,
|
|
@@ -1439,7 +1579,7 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
|
|
|
1439
1579
|
credentials: config.credentials,
|
|
1440
1580
|
signal,
|
|
1441
1581
|
...hasBody && {
|
|
1442
|
-
body: JSON.stringify(
|
|
1582
|
+
body: JSON.stringify(plan.remainingArgs)
|
|
1443
1583
|
}
|
|
1444
1584
|
});
|
|
1445
1585
|
if (!res.ok) {
|
|
@@ -1462,36 +1602,6 @@ async function throwForErrorResponse(res, config, fallbackBody) {
|
|
|
1462
1602
|
}
|
|
1463
1603
|
throw new ApiError("HTTP_ERROR", res.status, { body });
|
|
1464
1604
|
}
|
|
1465
|
-
function extractParamNames(path) {
|
|
1466
|
-
const matches = path.match(/:(\w+)/g);
|
|
1467
|
-
return matches ? matches.map((m) => m.slice(1)) : [];
|
|
1468
|
-
}
|
|
1469
|
-
function buildFetchUrl(baseUrl, prefix, path, args, pathPrefix = "") {
|
|
1470
|
-
let fullPath = `/${pathPrefix}${prefix}${path === "/" ? "" : path}`;
|
|
1471
|
-
if (args) {
|
|
1472
|
-
fullPath = fullPath.replace(/:(\w+)/g, (_, key) => {
|
|
1473
|
-
const val = args[key];
|
|
1474
|
-
if (val === undefined || val === null) {
|
|
1475
|
-
throw new Error(`Missing path param: ${key}`);
|
|
1476
|
-
}
|
|
1477
|
-
return encodeURIComponent(String(val));
|
|
1478
|
-
});
|
|
1479
|
-
}
|
|
1480
|
-
return `${baseUrl}${fullPath}`;
|
|
1481
|
-
}
|
|
1482
|
-
function stripParams(args, path, extra) {
|
|
1483
|
-
const skip = new Set(extra);
|
|
1484
|
-
for (const match of path.matchAll(/:(\w+)/g)) {
|
|
1485
|
-
if (match[1])
|
|
1486
|
-
skip.add(match[1]);
|
|
1487
|
-
}
|
|
1488
|
-
const result = {};
|
|
1489
|
-
for (const [k, v] of Object.entries(args)) {
|
|
1490
|
-
if (!skip.has(k))
|
|
1491
|
-
result[k] = v;
|
|
1492
|
-
}
|
|
1493
|
-
return result;
|
|
1494
|
-
}
|
|
1495
1605
|
|
|
1496
1606
|
// src/tools/remote.ts
|
|
1497
1607
|
var HTTP_ONLY = Object.freeze(["HTTP"]);
|
|
@@ -1514,7 +1624,7 @@ function implementRemote(contract, http, options) {
|
|
|
1514
1624
|
serviceName: contract.meta.prefix,
|
|
1515
1625
|
key,
|
|
1516
1626
|
toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
|
|
1517
|
-
expose: endpoint.rawResponse || endpoint.rawBody ? HTTP_ONLY : endpoint.expose,
|
|
1627
|
+
expose: endpoint.rawResponse || endpoint.rawBody || endpoint.responseMeta ? HTTP_ONLY : endpoint.expose,
|
|
1518
1628
|
ui: "ui" in endpoint ? endpoint.ui : undefined,
|
|
1519
1629
|
annotations: "annotations" in endpoint ? endpoint.annotations : undefined,
|
|
1520
1630
|
meta: mergeMeta(contract.meta.meta, endpoint.meta),
|
|
@@ -1527,6 +1637,7 @@ function implementRemote(contract, http, options) {
|
|
|
1527
1637
|
idempotent: endpoint.idempotent,
|
|
1528
1638
|
rawResponse: endpoint.rawResponse,
|
|
1529
1639
|
rawBody: endpoint.rawBody,
|
|
1640
|
+
responseMeta: endpoint.responseMeta,
|
|
1530
1641
|
contentType: "contentType" in endpoint ? endpoint.contentType : undefined,
|
|
1531
1642
|
handler: async (ctx) => {
|
|
1532
1643
|
const call = client[key];
|
|
@@ -1751,8 +1862,10 @@ export {
|
|
|
1751
1862
|
flattenToolJsonSchema,
|
|
1752
1863
|
findUntypedProperties,
|
|
1753
1864
|
findNonPortableFormats,
|
|
1865
|
+
defineRuntimeTool,
|
|
1754
1866
|
createToolkit,
|
|
1755
1867
|
createToolLogger,
|
|
1868
|
+
createToolInvoker,
|
|
1756
1869
|
createStdioMcpServer,
|
|
1757
1870
|
createMcpHandler,
|
|
1758
1871
|
createCli,
|