stitchkit 0.24.0 → 0.26.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/dist/browser/client-multipart.d.ts +22 -0
- package/dist/browser/client-multipart.d.ts.map +1 -0
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/contract/define.d.ts +20 -0
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/factory.d.ts +1 -0
- package/dist/contract/factory.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/{index-4gawbm74.js → index-0d0rb85d.js} +41 -3
- package/dist/{index-g8kyab85.js → index-bkccbx64.js} +10 -3
- package/dist/{index-x62gnfsk.js → index-h4y2wg3n.js} +1 -1
- package/dist/{index-a9n8m4ec.js → index-p6fge9a5.js} +1 -1
- package/dist/{index-x8m8e7dc.js → index-q5w3cvvp.js} +176 -87
- package/dist/index.js +20 -18
- package/dist/internal/errors.d.ts +6 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/write-download.d.ts +17 -0
- package/dist/internal/write-download.d.ts.map +1 -0
- package/dist/node.js +2 -2
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/error-hook.d.ts +14 -3
- package/dist/server/error-hook.d.ts.map +1 -1
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.js +6 -6
- package/dist/server/types.d.ts +13 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/agent.d.ts +2 -0
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/cli.d.ts.map +1 -1
- package/dist/tools/coerce.d.ts +2 -1
- package/dist/tools/coerce.d.ts.map +1 -1
- package/dist/tools/execute.d.ts +1 -1
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/flatten.d.ts +16 -6
- package/dist/tools/flatten.d.ts.map +1 -1
- package/dist/tools/list-names.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +10 -3
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount-download.d.ts.map +1 -1
- package/dist/tools/mount-upload.d.ts.map +1 -1
- package/dist/tools/mount-wait.d.ts.map +1 -1
- package/dist/tools/mount.d.ts +13 -0
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/names.d.ts +34 -0
- package/dist/tools/names.d.ts.map +1 -1
- package/dist/tools/remote.d.ts.map +1 -1
- package/dist/tools/schema.d.ts +43 -1
- package/dist/tools/schema.d.ts.map +1 -1
- package/dist/tools/transports.d.ts.map +1 -1
- package/dist/tools.js +48 -40
- package/llms-full.txt +113 -11
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -2,11 +2,13 @@ import {
|
|
|
2
2
|
inputIsQuery,
|
|
3
3
|
signJwt,
|
|
4
4
|
verifyPkce
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-p6fge9a5.js";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_CORS_ALLOW_HEADERS
|
|
8
8
|
} from "./index-tje0q6gp.js";
|
|
9
9
|
import {
|
|
10
|
+
assertToolName,
|
|
11
|
+
assertUniqueToolName,
|
|
10
12
|
coerceJsonArgs,
|
|
11
13
|
collectTools,
|
|
12
14
|
createCli,
|
|
@@ -17,15 +19,17 @@ import {
|
|
|
17
19
|
formatToolError,
|
|
18
20
|
pollUntil,
|
|
19
21
|
readCapped,
|
|
20
|
-
toolResultFromError
|
|
21
|
-
|
|
22
|
+
toolResultFromError,
|
|
23
|
+
writeDownload
|
|
24
|
+
} from "./index-q5w3cvvp.js";
|
|
22
25
|
import {
|
|
23
26
|
toJsonSchema
|
|
24
27
|
} from "./index-0ed3bx43.js";
|
|
25
28
|
import {
|
|
26
29
|
AppError,
|
|
27
|
-
isWithinDir
|
|
28
|
-
|
|
30
|
+
isWithinDir,
|
|
31
|
+
mergeMeta
|
|
32
|
+
} from "./index-0d0rb85d.js";
|
|
29
33
|
import"./index-khwedj16.js";
|
|
30
34
|
import {
|
|
31
35
|
isRecord,
|
|
@@ -44,16 +48,15 @@ function mountAgent(services, config = {}) {
|
|
|
44
48
|
hooks: config.hooks,
|
|
45
49
|
lifecycle: config.lifecycle,
|
|
46
50
|
errorHint: config.errorHint,
|
|
47
|
-
coerceJsonArgs: config.coerceJsonArgs
|
|
51
|
+
coerceJsonArgs: config.coerceJsonArgs,
|
|
52
|
+
onOutputStrip: config.onOutputStrip
|
|
48
53
|
});
|
|
49
54
|
for (const service of serviceList) {
|
|
50
55
|
for (const mountable of collectTools(service, "AGENT", {
|
|
51
56
|
extend: config.extend,
|
|
52
57
|
flattenUnionInput: config.flattenUnionInput
|
|
53
58
|
})) {
|
|
54
|
-
|
|
55
|
-
throw new Error(`Duplicate agent tool name "${mountable.name}" across mounted services`);
|
|
56
|
-
}
|
|
59
|
+
assertUniqueToolName(mountable.name, Object.hasOwn(tools, mountable.name), "agent tool name");
|
|
57
60
|
tools[mountable.name] = tool({
|
|
58
61
|
description: mountable.method.desc,
|
|
59
62
|
inputSchema: zodSchema(mountable.schema),
|
|
@@ -79,7 +82,7 @@ function listToolNames(services) {
|
|
|
79
82
|
const entries = new Map;
|
|
80
83
|
for (const service of services) {
|
|
81
84
|
for (const transport of TOOL_TRANSPORTS) {
|
|
82
|
-
for (const tool2 of collectTools(service, transport)) {
|
|
85
|
+
for (const tool2 of collectTools(service, transport, { assertNames: false })) {
|
|
83
86
|
const id = `${service.name}\x00${tool2.method.key}`;
|
|
84
87
|
const existing = entries.get(id);
|
|
85
88
|
if (existing) {
|
|
@@ -188,9 +191,7 @@ function throwIfFailures(failures) {
|
|
|
188
191
|
}
|
|
189
192
|
}
|
|
190
193
|
function prepareMcpTool(mountable, policy, logger, failures, seen) {
|
|
191
|
-
|
|
192
|
-
throw new Error(`Duplicate MCP tool name "${mountable.name}" across mounted services`);
|
|
193
|
-
}
|
|
194
|
+
assertUniqueToolName(mountable.name, seen.has(mountable.name), "MCP tool name");
|
|
194
195
|
seen.add(mountable.name);
|
|
195
196
|
if (!(mountable.schema instanceof z.ZodObject)) {
|
|
196
197
|
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);
|
|
@@ -231,7 +232,8 @@ function mountMcp(mcpServer, services, config = {}) {
|
|
|
231
232
|
hooks: config.hooks,
|
|
232
233
|
lifecycle: config.lifecycle,
|
|
233
234
|
errorHint: config.errorHint,
|
|
234
|
-
coerceJsonArgs: config.coerceJsonArgs
|
|
235
|
+
coerceJsonArgs: config.coerceJsonArgs,
|
|
236
|
+
onOutputStrip: config.onOutputStrip
|
|
235
237
|
});
|
|
236
238
|
const seen = new Set;
|
|
237
239
|
const failures = [];
|
|
@@ -283,7 +285,7 @@ function buildMcpServer(config, auth) {
|
|
|
283
285
|
flattenUnionInput: config.flattenUnionInput,
|
|
284
286
|
errorHint: config.errorHint
|
|
285
287
|
});
|
|
286
|
-
config.nativeTools?.(server);
|
|
288
|
+
config.nativeTools?.(server, auth);
|
|
287
289
|
for (const resource of config.resources ?? []) {
|
|
288
290
|
mountMcpResource(server, resource);
|
|
289
291
|
}
|
|
@@ -490,7 +492,6 @@ async function createStdioMcpServer(config) {
|
|
|
490
492
|
return server;
|
|
491
493
|
}
|
|
492
494
|
// src/tools/mount-download.ts
|
|
493
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
494
495
|
import { basename, extname, join } from "node:path";
|
|
495
496
|
|
|
496
497
|
// src/tools/native-result.ts
|
|
@@ -531,7 +532,9 @@ function baseNameFor(url) {
|
|
|
531
532
|
}
|
|
532
533
|
}
|
|
533
534
|
function mountDownload(server, config) {
|
|
534
|
-
|
|
535
|
+
const name = config.name ?? "download";
|
|
536
|
+
assertToolName(name, "<native>", "download");
|
|
537
|
+
server.registerTool(name, { description: config.description, inputSchema: config.inputSchema }, async (rawArgs) => {
|
|
535
538
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
536
539
|
try {
|
|
537
540
|
const url = await config.resolveUrl(args);
|
|
@@ -551,9 +554,8 @@ function mountDownload(server, config) {
|
|
|
551
554
|
if (!buffer)
|
|
552
555
|
return textResult(`Download failed: file exceeds the ${max}-byte cap`, true);
|
|
553
556
|
const dir = config.dirFromArgs?.(args) ?? config.defaultDir;
|
|
554
|
-
await mkdir(dir, { recursive: true });
|
|
555
557
|
const filePath = join(dir, `${baseNameFor(url)}${extensionFor(url, mimeType)}`);
|
|
556
|
-
await
|
|
558
|
+
await writeDownload(dir, filePath, buffer);
|
|
557
559
|
return textResult(JSON.stringify({ path: filePath, size: buffer.length, mimeType }, null, 2));
|
|
558
560
|
} catch (err) {
|
|
559
561
|
return textResult(`Download failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
@@ -563,7 +565,9 @@ function mountDownload(server, config) {
|
|
|
563
565
|
// src/tools/mount-upload.ts
|
|
564
566
|
import { z as z2 } from "zod";
|
|
565
567
|
function mountUpload(server, config) {
|
|
566
|
-
|
|
568
|
+
const name = config.name ?? "upload";
|
|
569
|
+
assertToolName(name, "<native>", "upload");
|
|
570
|
+
server.registerTool(name, {
|
|
567
571
|
description: config.description,
|
|
568
572
|
inputSchema: { path: z2.string().describe("Path to a local file on this machine") }
|
|
569
573
|
}, async (rawArgs) => {
|
|
@@ -581,7 +585,9 @@ function mountUpload(server, config) {
|
|
|
581
585
|
}
|
|
582
586
|
// src/tools/mount-wait.ts
|
|
583
587
|
function mountWait(server, config) {
|
|
584
|
-
|
|
588
|
+
const name = config.name ?? "wait";
|
|
589
|
+
assertToolName(name, "<native>", "wait");
|
|
590
|
+
server.registerTool(name, { description: config.description, inputSchema: config.inputSchema }, async (rawArgs) => {
|
|
585
591
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
586
592
|
try {
|
|
587
593
|
const { state, timedOut } = await pollUntil({
|
|
@@ -870,6 +876,25 @@ function mountOAuthProvider(config) {
|
|
|
870
876
|
};
|
|
871
877
|
return [metadataRoute, registerRoute, authorizeRoute, tokenRoute];
|
|
872
878
|
}
|
|
879
|
+
// src/browser/client-multipart.ts
|
|
880
|
+
function isFileDescriptor(value) {
|
|
881
|
+
return typeof value === "object" && value !== null && !(value instanceof Blob) && "uri" in value && typeof value.uri === "string" && "name" in value && typeof value.name === "string" && "type" in value && typeof value.type === "string";
|
|
882
|
+
}
|
|
883
|
+
function isMultipartFile(value) {
|
|
884
|
+
return value instanceof Blob || isFileDescriptor(value);
|
|
885
|
+
}
|
|
886
|
+
function appendMultipartFile(form, field, file) {
|
|
887
|
+
const sink = form;
|
|
888
|
+
sink.append(field, file);
|
|
889
|
+
}
|
|
890
|
+
function appendFormFields(formData, values, skipKeys) {
|
|
891
|
+
for (const [key, value] of Object.entries(values)) {
|
|
892
|
+
if (skipKeys.has(key) || value === undefined || value === null)
|
|
893
|
+
continue;
|
|
894
|
+
formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
873
898
|
// src/browser/http.ts
|
|
874
899
|
import ky, { isHTTPError } from "ky";
|
|
875
900
|
class ApiError extends Error {
|
|
@@ -1079,23 +1104,6 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
|
|
|
1079
1104
|
return endpoint.output ? endpoint.output.parse(json2) : json2;
|
|
1080
1105
|
};
|
|
1081
1106
|
}
|
|
1082
|
-
function isFileDescriptor(value) {
|
|
1083
|
-
return typeof value === "object" && value !== null && !(value instanceof Blob) && "uri" in value && typeof value.uri === "string" && "name" in value && typeof value.name === "string" && "type" in value && typeof value.type === "string";
|
|
1084
|
-
}
|
|
1085
|
-
function isMultipartFile(value) {
|
|
1086
|
-
return value instanceof Blob || isFileDescriptor(value);
|
|
1087
|
-
}
|
|
1088
|
-
function appendMultipartFile(form, field, file) {
|
|
1089
|
-
const sink = form;
|
|
1090
|
-
sink.append(field, file);
|
|
1091
|
-
}
|
|
1092
|
-
function appendFormFields(formData, values, skipKeys) {
|
|
1093
|
-
for (const [key, value] of Object.entries(values)) {
|
|
1094
|
-
if (skipKeys.has(key) || value === undefined || value === null)
|
|
1095
|
-
continue;
|
|
1096
|
-
formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
1107
|
async function throwForErrorResponse(res, config, fallbackBody) {
|
|
1100
1108
|
const body = await res.json().catch(() => fallbackBody);
|
|
1101
1109
|
config.onError?.(res.status, body);
|
|
@@ -1159,7 +1167,7 @@ function implementRemote(contract, http, options) {
|
|
|
1159
1167
|
expose: endpoint.expose,
|
|
1160
1168
|
ui: "ui" in endpoint ? endpoint.ui : undefined,
|
|
1161
1169
|
annotations: "annotations" in endpoint ? endpoint.annotations : undefined,
|
|
1162
|
-
meta: endpoint.meta,
|
|
1170
|
+
meta: mergeMeta(contract.meta.meta, endpoint.meta),
|
|
1163
1171
|
scope: endpoint.scope ?? groupScope,
|
|
1164
1172
|
paramsSchema: endpoint.params,
|
|
1165
1173
|
inputSchema: endpoint.input,
|
|
@@ -1236,7 +1244,7 @@ function summarizeTransports(services) {
|
|
|
1236
1244
|
counts.HTTP += 1;
|
|
1237
1245
|
}
|
|
1238
1246
|
for (const transport of TOOL_TRANSPORTS2) {
|
|
1239
|
-
counts[transport] = collectTools(service, transport).length;
|
|
1247
|
+
counts[transport] = collectTools(service, transport, { assertNames: false }).length;
|
|
1240
1248
|
}
|
|
1241
1249
|
for (const transport of Object.keys(counts)) {
|
|
1242
1250
|
totals[transport] += counts[transport];
|
package/llms-full.txt
CHANGED
|
@@ -237,7 +237,7 @@ export const users = defineContract({ prefix: 'users' }, {
|
|
|
237
237
|
| `output` | no | Zod schema for the **response body** |
|
|
238
238
|
| `scope` | no | access scope for this endpoint — see [Auth & errors](./auth-and-errors.md) |
|
|
239
239
|
| `expose` | no | which transports carry this endpoint — see [below](#transports) |
|
|
240
|
-
| `toolName` | no | explicit MCP / agent tool name (
|
|
240
|
+
| `toolName` | no | explicit MCP / agent tool name (default: a verb-aware derivation, see below — not a literal `prefix_key`) |
|
|
241
241
|
| `multipart` | no | field name of a file upload — see [below](#file-uploads) |
|
|
242
242
|
| `timeout` | no | per-endpoint client timeout in ms, for slow endpoints |
|
|
243
243
|
| `idempotent` | no | safe to call twice with the same input (like `PUT`/`DELETE`); a retrying transport reads it — see [Realtime](./realtime.md#bring-your-own-transport) |
|
|
@@ -326,8 +326,41 @@ derivation from the method key + prefix (`users` + `create` ⇒ `create_user`,
|
|
|
326
326
|
{ method: 'POST', path: '/', desc: 'Create a user', toolName: 'create_user', /* … */ }
|
|
327
327
|
```
|
|
328
328
|
|
|
329
|
+
**Every tool name — derived or explicit — must match `[a-zA-Z0-9_-]` and be at
|
|
330
|
+
most 64 characters**, the character class every major provider accepts.
|
|
331
|
+
|
|
332
|
+
Note the two classes differ. What is *accepted* includes the hyphen; what
|
|
333
|
+
derivation *keeps* does not — a derived name normalises everything outside
|
|
334
|
+
`[a-zA-Z0-9_]` to `_`, the hyphen included, so `bot-status` + `get` ⇒
|
|
335
|
+
`get_bot_status` and `admin/analytics` + `get` ⇒ `get_admin_analytics`. A hyphen
|
|
336
|
+
survives only in an explicit `toolName`, which is taken verbatim.
|
|
337
|
+
|
|
338
|
+
A prefix with *no* usable character at all (`'///'`, `'_'`, a fully non-ASCII
|
|
339
|
+
prefix) and any explicit `toolName` outside the accepted class **throw at
|
|
340
|
+
mount**. An unusable prefix is rescued by setting an explicit `toolName` — the
|
|
341
|
+
prefix then never enters the name. An over-long name is fixed by a shorter
|
|
342
|
+
explicit `toolName` (or a shorter prefix / method key). Nothing downstream checks this: the provider rejects the whole
|
|
343
|
+
request, so one bad name takes every tool of that mount down with it.
|
|
344
|
+
→ [ADR 0035](../decisions/0035-tool-name-derivation-and-validation.md).
|
|
345
|
+
|
|
329
346
|
## Endpoint metadata (`meta`)
|
|
330
347
|
|
|
348
|
+
A contract can declare a **default** `meta` that every endpoint inherits, and an
|
|
349
|
+
endpoint's own keys are **shallow-merged over** it:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
defineContract({ prefix: 'admin', meta: { public: true } }, {
|
|
353
|
+
list: { method: 'GET', path: '/', desc: 'List', /* meta → { public: true } */ },
|
|
354
|
+
purge: { method: 'POST', path: '/purge', desc: 'Purge', meta: { rateTier: 2 } },
|
|
355
|
+
// meta → { public: true, rateTier: 2 }
|
|
356
|
+
})
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
One level deep — no deep merge, no way to unset an inherited key; declare it on
|
|
360
|
+
the endpoint instead. `expose` deliberately has **no** contract-level equivalent
|
|
361
|
+
(→ [ADR 0036](../decisions/0036-contract-level-meta.md)).
|
|
362
|
+
|
|
363
|
+
|
|
331
364
|
`meta` is an **opaque, app-defined** bag the core attaches no meaning to — the
|
|
332
365
|
same escape-hatch spirit as `scope` being a free string ([ADR 0002](../decisions/0002-generic-core.md) /
|
|
333
366
|
[ADR 0021](../decisions/0021-endpoint-meta-passthrough.md)). Declare app concerns
|
|
@@ -1172,7 +1205,14 @@ By default every endpoint is a tool on every transport. `expose` narrows it:
|
|
|
1172
1205
|
`desc` is the tool description the model reads — write it for the model, not
|
|
1173
1206
|
just for a human. A `multipart` endpoint is never a tool. The tool name defaults
|
|
1174
1207
|
to a verb-aware name from the method + prefix (`list` → `list_widgets`, `get` →
|
|
1175
|
-
`get_widget`); set `toolName` for an explicit one.
|
|
1208
|
+
`get_widget`); set `toolName` for an explicit one. Derivation normalises every
|
|
1209
|
+
character outside `[a-zA-Z0-9_]` to `_` — the hyphen included, so `bot-status`
|
|
1210
|
+
derives `get_bot_status` — while a name is *accepted* if it matches
|
|
1211
|
+
`[a-zA-Z0-9_-]`, so a hyphen survives in an explicit `toolName`. A name that
|
|
1212
|
+
still cannot be delivered (illegal explicit `toolName`, over 64 characters, or a
|
|
1213
|
+
prefix with no usable character) throws at mount rather than at the first model
|
|
1214
|
+
call —
|
|
1215
|
+
→ [ADR 0035](../decisions/0035-tool-name-derivation-and-validation.md). See
|
|
1176
1216
|
[Contracts → transports](./contracts.md#transports).
|
|
1177
1217
|
|
|
1178
1218
|
### Pinning tool names — `listToolNames`
|
|
@@ -1196,6 +1236,14 @@ now fails this test instead of silently breaking the clients that call the
|
|
|
1196
1236
|
tool. It is also the mechanical diff when migrating a service: run it before
|
|
1197
1237
|
and after, compare.
|
|
1198
1238
|
|
|
1239
|
+
> **This is also the guard against a forgotten `expose`.** An endpoint that
|
|
1240
|
+
> declares none is a tool on MCP **and** AGENT — the default is fail-open, and
|
|
1241
|
+
> there is no contract-level `expose` to set once (→
|
|
1242
|
+
> [ADR 0036](../decisions/0036-contract-level-meta.md)). A snapshot of
|
|
1243
|
+
> `listToolNames` fails the build the moment an endpoint you meant to keep
|
|
1244
|
+
> HTTP-only shows up in the list, which is the one check that catches it however
|
|
1245
|
+
> many places the line was forgotten.
|
|
1246
|
+
|
|
1199
1247
|
## MCP — `createMcpHandler`
|
|
1200
1248
|
|
|
1201
1249
|
`createMcpHandler` builds a complete Streamable-HTTP MCP server as a single
|
|
@@ -1234,11 +1282,11 @@ createServer({
|
|
|
1234
1282
|
| `hooks` | tool-call observability hooks — `afterToolCall` fires on every result |
|
|
1235
1283
|
| `onIncompatibleSchema` | `'throw'` (default) · `'skip'` · `'warn'` — see below |
|
|
1236
1284
|
| `logger` | a `StitchLogger` for the `'warn'` policy |
|
|
1237
|
-
| `nativeTools` | register non-contract tools directly on the `McpServer` |
|
|
1285
|
+
| `nativeTools` | `(server, auth) => …` — register non-contract tools directly on the `McpServer`; receives the resolved identity, but is **not** a scope gate (`lifecycle` does not run for native tools) |
|
|
1238
1286
|
| `instructions` | a short host-facing usage hint, surfaced to MCP tool-search |
|
|
1239
1287
|
|
|
1240
|
-
`services` and `
|
|
1241
|
-
shown only its own tools and every handler can read `ctx.tenantId`.
|
|
1288
|
+
`services`, `context` and `nativeTools` all receive the resolved identity, so a
|
|
1289
|
+
tenant can be shown only its own tools and every handler can read `ctx.tenantId`.
|
|
1242
1290
|
|
|
1243
1291
|
### Guarding tools — `lifecycle`
|
|
1244
1292
|
|
|
@@ -1303,11 +1351,23 @@ each variant's fields become optional with a `Required if <disc> = …` hint.
|
|
|
1303
1351
|
It is **deep**: unions are flattened at every depth — top level, object fields,
|
|
1304
1352
|
array items, and through `optional` / `nullable` / `default` / intersection
|
|
1305
1353
|
wrappers — so no `oneOf` survives anywhere (e.g. a `content.parts[]` that is an
|
|
1306
|
-
array of a discriminated union).
|
|
1307
|
-
schemas stay the validation schemas in `executeToolMethod`, so requests are still
|
|
1308
|
-
validated against the real union. Schemas a transform cannot safely rebuild
|
|
1354
|
+
array of a discriminated union). Schemas a transform cannot safely rebuild
|
|
1309
1355
|
(refined / piped / lazy / plain non-discriminated unions) are left as-is.
|
|
1310
1356
|
|
|
1357
|
+
The flattened form is **lossy but never destructive**. Lossy: per-variant
|
|
1358
|
+
strictness and object-level refinements are not advertised — the original schemas
|
|
1359
|
+
enforce them in `executeToolMethod`. Not destructive: every object keeps its own
|
|
1360
|
+
**key policy** (`.strict()` stays strict, `.loose()` / `.catchall()` still keep
|
|
1361
|
+
extra keys), because the advertised schema is not advertised-only — the MCP and
|
|
1362
|
+
AI SDKs parse the caller's arguments *with it* and hand the handler the parsed
|
|
1363
|
+
result. An object advertised without its policy would silently delete keys the
|
|
1364
|
+
contract would have rejected. → [ADR 0034](../decisions/0034-advertised-schema-key-policy.md).
|
|
1365
|
+
|
|
1366
|
+
A consequence worth knowing when you read logs: a `.strict()` violation is caught
|
|
1367
|
+
by the SDK **before** the tool callback runs, so it comes back as an MCP
|
|
1368
|
+
`InvalidParams` protocol error rather than a stitchkit `VALIDATION_ERROR`
|
|
1369
|
+
envelope, and `beforeToolCall` / `afterToolCall` do not fire for it.
|
|
1370
|
+
|
|
1311
1371
|
## `mountMcp`
|
|
1312
1372
|
|
|
1313
1373
|
If you already run an `McpServer` from the SDK, `mountMcp` adds contract tools
|
|
@@ -2467,7 +2527,13 @@ const onError = createErrorHook({
|
|
|
2467
2527
|
CONFLICT: 'conflict', RATE_LIMITED: 'rate_limited',
|
|
2468
2528
|
INTERNAL_SERVER_ERROR: 'internal',
|
|
2469
2529
|
} satisfies Record<StitchErrorCode, string>,
|
|
2470
|
-
|
|
2530
|
+
// `ctx` is the request's RuntimeContext — read `ctx.traceId` for a
|
|
2531
|
+
// correlation id in the envelope. Declaring it is optional.
|
|
2532
|
+
render: (info, ctx) => ({
|
|
2533
|
+
ok: false,
|
|
2534
|
+
error: { code: info.code, message: info.message },
|
|
2535
|
+
traceId: ctx.traceId,
|
|
2536
|
+
}),
|
|
2471
2537
|
})
|
|
2472
2538
|
|
|
2473
2539
|
createServer({ services, hooks: { onError } })
|
|
@@ -3111,6 +3177,42 @@ current one *up to* your target, and apply each snippet.
|
|
|
3111
3177
|
(`STITCH_ERROR_STATUS`, `serveFile`, `scopePrefixes`, `afterToolCall`'s
|
|
3112
3178
|
`MethodDef`, `maxUploadBytes`) are available to adopt, not required.
|
|
3113
3179
|
|
|
3180
|
+
## Your handlers may be returning more than the contract declares
|
|
3181
|
+
|
|
3182
|
+
stitchkit validates every handler's return value against the endpoint's `output`
|
|
3183
|
+
schema and **passes on the parsed result** — so any field the schema does not
|
|
3184
|
+
declare is silently removed. That is deliberate (the contract is the published
|
|
3185
|
+
shape of the response), but when you are moving a *live* API onto stitchkit it is
|
|
3186
|
+
invisible: TypeScript does not reject excess properties, nothing logs it, and the
|
|
3187
|
+
client just receives fewer fields.
|
|
3188
|
+
|
|
3189
|
+
While migrating, turn the diagnostic on:
|
|
3190
|
+
|
|
3191
|
+
```ts
|
|
3192
|
+
createServer({ services, warnOnOutputStrip: true }) // off by default
|
|
3193
|
+
```
|
|
3194
|
+
|
|
3195
|
+
Every removed key is logged as a dot-path with the endpoint that produced it
|
|
3196
|
+
(`notes.get: secret, nested.alsoSecret`). Tool transports strip identically —
|
|
3197
|
+
`mountMcp` / `mountAgent` take `onOutputStrip: (toolName, paths) => …`. Read the
|
|
3198
|
+
list, then either widen the contract or stop returning the field, and turn the
|
|
3199
|
+
flag back off: it is for the migration window, not for production.
|
|
3200
|
+
|
|
3201
|
+
## Tool names may shift between versions
|
|
3202
|
+
|
|
3203
|
+
Derived tool names are part of your public surface — an MCP client config or an
|
|
3204
|
+
agent prompt refers to them by string. Before and after any upgrade that touches
|
|
3205
|
+
name derivation, diff them mechanically:
|
|
3206
|
+
|
|
3207
|
+
```ts
|
|
3208
|
+
import { listToolNames } from 'stitchkit/tools'
|
|
3209
|
+
console.log(JSON.stringify(listToolNames(services), null, 2))
|
|
3210
|
+
```
|
|
3211
|
+
|
|
3212
|
+
`listToolNames` never throws on an illegal name — that is deliberate, so it can
|
|
3213
|
+
show you the offending row when a mount would refuse it. Pin it in a snapshot
|
|
3214
|
+
test and a shift fails your build instead of your clients.
|
|
3215
|
+
|
|
3114
3216
|
## When you author a breaking change in stitchkit
|
|
3115
3217
|
|
|
3116
3218
|
You are on the other side of this flow — see
|
|
@@ -3200,7 +3302,7 @@ from the root `stitchkit`.
|
|
|
3200
3302
|
| `ScopedDefineContract` | _type_ | the `defineContract` `createContractFactory` returns |
|
|
3201
3303
|
| `ALL_TRANSPORTS` | constant | `['HTTP', 'MCP', 'AGENT', 'CLI']` |
|
|
3202
3304
|
| `ContractDef` | _type_ | a defined contract |
|
|
3203
|
-
| `ContractMeta` | _type_ | a contract's `prefix` + optional `scope` |
|
|
3305
|
+
| `ContractMeta` | _type_ | a contract's `prefix` + optional `scope` and `meta` (a default every endpoint shallow-merges over) |
|
|
3204
3306
|
| `EndpointDef` | _type_ | a single endpoint definition |
|
|
3205
3307
|
| `HttpMethod` | _type_ | `GET \| POST \| PUT \| PATCH \| DELETE` |
|
|
3206
3308
|
| `Transport` | _type_ | `HTTP \| MCP \| AGENT \| CLI` |
|
|
@@ -3529,7 +3631,7 @@ Advanced building blocks — the shared machinery the mounts are built on.
|
|
|
3529
3631
|
| `TransportCounts` | _type_ | per-transport counts (`{ HTTP, MCP, AGENT, CLI }`) |
|
|
3530
3632
|
| `coerceJsonArgs` | function | coerce JSON-stringified array/object tool arguments |
|
|
3531
3633
|
| `flattenDiscriminatedUnion` | function | flatten one discriminated union into a single object schema |
|
|
3532
|
-
| `flattenUnionsDeep` | function | flatten discriminated unions at every depth
|
|
3634
|
+
| `flattenUnionsDeep` | function | flatten discriminated unions at every depth — union shape only; each object keeps its own key policy (`.strict()` / `.loose()` / `.catchall()`) |
|
|
3533
3635
|
| `MountableTool` | _type_ | one contract method resolved for mounting |
|
|
3534
3636
|
| `ToolManifestEntry` | _type_ | one `buildToolManifest` row |
|
|
3535
3637
|
|
package/package.json
CHANGED