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.
Files changed (61) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/client-url.d.ts +13 -0
  3. package/dist/browser/client-url.d.ts.map +1 -0
  4. package/dist/browser/client.d.ts +41 -5
  5. package/dist/browser/client.d.ts.map +1 -1
  6. package/dist/browser/http.d.ts +10 -7
  7. package/dist/browser/http.d.ts.map +1 -1
  8. package/dist/cli.js +2 -2
  9. package/dist/contract/define.d.ts +102 -4
  10. package/dist/contract/define.d.ts.map +1 -1
  11. package/dist/contract/index.d.ts +1 -1
  12. package/dist/contract/index.d.ts.map +1 -1
  13. package/dist/contract/index.js +1 -1
  14. package/dist/{index-czmqks7r.js → index-6jypn22c.js} +1 -1
  15. package/dist/{index-4pvtq6h2.js → index-7rbzbnnf.js} +104 -30
  16. package/dist/{index-xq45akyd.js → index-92gs1m5b.js} +1 -1
  17. package/dist/{index-e497hxcy.js → index-fyfk537k.js} +50 -5
  18. package/dist/{index-g3jrbd0z.js → index-xax049k6.js} +101 -5
  19. package/dist/{index-s6yhmg1k.js → index-y5scd1cr.js} +3 -1
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +243 -111
  23. package/dist/internal/route-pattern.d.ts +7 -0
  24. package/dist/internal/route-pattern.d.ts.map +1 -0
  25. package/dist/node.js +3 -3
  26. package/dist/observability/index.js +1 -1
  27. package/dist/react/entity-cache.d.ts +56 -42
  28. package/dist/react/entity-cache.d.ts.map +1 -1
  29. package/dist/react.d.ts +1 -1
  30. package/dist/react.d.ts.map +1 -1
  31. package/dist/react.js +98 -46
  32. package/dist/server/create.d.ts.map +1 -1
  33. package/dist/server/implement.d.ts.map +1 -1
  34. package/dist/server/index.js +22 -7
  35. package/dist/server/middleware/cors.d.ts.map +1 -1
  36. package/dist/server/openapi.d.ts.map +1 -1
  37. package/dist/server/response-metadata.d.ts +9 -0
  38. package/dist/server/response-metadata.d.ts.map +1 -0
  39. package/dist/server/router.d.ts +2 -0
  40. package/dist/server/router.d.ts.map +1 -1
  41. package/dist/server/socket-io.d.ts +1 -1
  42. package/dist/server/types.d.ts +14 -8
  43. package/dist/server/types.d.ts.map +1 -1
  44. package/dist/tools/agent.d.ts +3 -0
  45. package/dist/tools/agent.d.ts.map +1 -1
  46. package/dist/tools/invoker.d.ts +33 -0
  47. package/dist/tools/invoker.d.ts.map +1 -0
  48. package/dist/tools/mcp.d.ts.map +1 -1
  49. package/dist/tools/mount.d.ts.map +1 -1
  50. package/dist/tools/names.d.ts +1 -1
  51. package/dist/tools/names.d.ts.map +1 -1
  52. package/dist/tools/native-mcp.d.ts +6 -33
  53. package/dist/tools/native-mcp.d.ts.map +1 -1
  54. package/dist/tools/remote.d.ts.map +1 -1
  55. package/dist/tools/runtime-tool.d.ts +58 -0
  56. package/dist/tools/runtime-tool.d.ts.map +1 -0
  57. package/dist/tools.d.ts +3 -1
  58. package/dist/tools.d.ts.map +1 -1
  59. package/dist/tools.js +295 -182
  60. package/llms-full.txt +562 -79
  61. 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-xq45akyd.js";
5
+ } from "./index-92gs1m5b.js";
6
6
  import {
7
7
  DEFAULT_CORS_ALLOW_HEADERS
8
- } from "./index-czmqks7r.js";
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-s6yhmg1k.js";
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-e497hxcy.js";
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
- 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
- };
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(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({
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: "Native MCP handler returned an invalid result" }
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
- formatError: (result, toolName) => formatMcpResult(result, "none", toolName, config.errorHint)
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
- let pathPrefixStr = "";
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, firstArg, new Set([...prefixKeys, endpoint.multipart]));
1357
- return withOutput(endpoint, client[httpMethod](url, formData, withTimeout(undefined, endpoint)));
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 (isGet) {
1360
- const params = collectQueryParams(firstArg, prefixKeys, endpoint);
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
- const params = collectQueryParams(firstArg, prefixKeys, endpoint);
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](url, Object.keys(payload).length > 0 ? payload : undefined, withTimeout(undefined, endpoint)));
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
- let pathPrefixStr = "";
1380
- if (contractConfig?.pathPrefix) {
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 isQuery = inputIsQuery(endpoint.method);
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, stripParams(args, endpoint.path, prefixKeys), new Set([endpoint.multipart]));
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(stripParams(hasBody, endpoint.path, prefixKeys))
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,