stitchkit 0.1.0 → 0.2.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 +10 -7
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/errors.d.ts +4 -3
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/{index-gfzn1n4n.js → index-a35v22fh.js} +1 -1
- package/dist/{index-7wfkbvss.js → index-kckky6zw.js} +6 -2
- package/dist/index-v2z2v3mq.js +587 -0
- package/dist/index.js +2 -2
- package/dist/node.d.ts +4 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +26 -0
- package/dist/server/create.d.ts +3 -3
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +10 -576
- package/dist/server/node.d.ts +12 -0
- package/dist/server/node.d.ts.map +1 -0
- package/dist/server/types.d.ts +24 -21
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/agent.d.ts +15 -2
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/coerce.d.ts +8 -0
- package/dist/tools/coerce.d.ts.map +1 -0
- package/dist/tools/execute.d.ts +26 -2
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/flatten.d.ts +15 -0
- package/dist/tools/flatten.d.ts.map +1 -0
- package/dist/tools/json-schema.d.ts +18 -0
- package/dist/tools/json-schema.d.ts.map +1 -0
- package/dist/tools/manifest.d.ts +13 -0
- package/dist/tools/manifest.d.ts.map +1 -0
- package/dist/tools/mcp-handler.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +53 -3
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount.d.ts +18 -6
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/schema.d.ts +19 -2
- package/dist/tools/schema.d.ts.map +1 -1
- package/dist/tools.d.ts +6 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +337 -154
- package/package.json +15 -4
package/dist/tools.js
CHANGED
|
@@ -1,23 +1,54 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createClient
|
|
3
3
|
} from "./index-5sxnvwb1.js";
|
|
4
|
-
import {
|
|
5
|
-
formatZodError,
|
|
6
|
-
normalizeError
|
|
7
|
-
} from "./index-gfzn1n4n.js";
|
|
8
|
-
import"./index-7wfkbvss.js";
|
|
9
4
|
import {
|
|
10
5
|
isRecord
|
|
11
6
|
} from "./index-809wc1tt.js";
|
|
7
|
+
import {
|
|
8
|
+
formatZodError,
|
|
9
|
+
normalizeError
|
|
10
|
+
} from "./index-a35v22fh.js";
|
|
11
|
+
import"./index-kckky6zw.js";
|
|
12
12
|
|
|
13
13
|
// src/tools/agent.ts
|
|
14
14
|
import { tool, zodSchema } from "ai";
|
|
15
15
|
|
|
16
|
-
// src/tools/
|
|
17
|
-
import { z
|
|
16
|
+
// src/tools/schema.ts
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
function objectShapeKeys(schema) {
|
|
19
|
+
return schema instanceof z.ZodObject ? Object.keys(schema.shape) : [];
|
|
20
|
+
}
|
|
21
|
+
function mergeSchemas(paramsSchema, inputSchema) {
|
|
22
|
+
if (paramsSchema && !(paramsSchema instanceof z.ZodObject)) {
|
|
23
|
+
throw new Error("Tool params schema must be a z.object()");
|
|
24
|
+
}
|
|
25
|
+
const paramsObject = paramsSchema instanceof z.ZodObject ? paramsSchema : undefined;
|
|
26
|
+
if (!inputSchema) {
|
|
27
|
+
return paramsObject ?? z.object({});
|
|
28
|
+
}
|
|
29
|
+
if (inputSchema instanceof z.ZodObject) {
|
|
30
|
+
if (paramsObject) {
|
|
31
|
+
const conflicts = Object.keys(paramsObject.shape).filter((key) => (key in inputSchema.shape));
|
|
32
|
+
if (conflicts.length > 0) {
|
|
33
|
+
throw new Error(`Schema merge conflict: ${conflicts.join(", ")} appear in both params and input`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return z.object({ ...paramsObject?.shape ?? {}, ...inputSchema.shape });
|
|
37
|
+
}
|
|
38
|
+
return paramsObject ? z.intersection(paramsObject, inputSchema) : inputSchema;
|
|
39
|
+
}
|
|
18
40
|
|
|
19
41
|
// src/tools/execute.ts
|
|
20
|
-
|
|
42
|
+
function toolResultFromError(err) {
|
|
43
|
+
const appErr = normalizeError(err);
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
code: appErr.code,
|
|
47
|
+
details: appErr.details ?? { message: appErr.message },
|
|
48
|
+
...appErr.hint && { hint: appErr.hint }
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function executeToolMethod(method, toolName, rawArgs, context, hooks, lifecycle) {
|
|
21
52
|
const startedAt = Date.now();
|
|
22
53
|
const finish = async (result) => {
|
|
23
54
|
await hooks?.afterToolCall?.(toolName, rawArgs, result, Date.now() - startedAt, context);
|
|
@@ -26,9 +57,18 @@ async function executeToolMethod(method, toolName, rawArgs, context, hooks) {
|
|
|
26
57
|
if (hooks?.beforeToolCall) {
|
|
27
58
|
await hooks.beforeToolCall(toolName, rawArgs, context);
|
|
28
59
|
}
|
|
60
|
+
const paramKeys = new Set(objectShapeKeys(method.paramsSchema));
|
|
61
|
+
const paramArgs = {};
|
|
62
|
+
const inputArgs = {};
|
|
63
|
+
for (const [key, value] of Object.entries(rawArgs)) {
|
|
64
|
+
if (paramKeys.has(key))
|
|
65
|
+
paramArgs[key] = value;
|
|
66
|
+
else
|
|
67
|
+
inputArgs[key] = value;
|
|
68
|
+
}
|
|
29
69
|
let params;
|
|
30
70
|
if (method.paramsSchema) {
|
|
31
|
-
const result = method.paramsSchema.safeParse(
|
|
71
|
+
const result = method.paramsSchema.safeParse(paramArgs);
|
|
32
72
|
if (!result.success) {
|
|
33
73
|
return finish({
|
|
34
74
|
ok: false,
|
|
@@ -40,7 +80,7 @@ async function executeToolMethod(method, toolName, rawArgs, context, hooks) {
|
|
|
40
80
|
}
|
|
41
81
|
let input;
|
|
42
82
|
if (method.inputSchema) {
|
|
43
|
-
const result = method.inputSchema.safeParse(
|
|
83
|
+
const result = method.inputSchema.safeParse(inputArgs);
|
|
44
84
|
if (!result.success) {
|
|
45
85
|
return finish({
|
|
46
86
|
ok: false,
|
|
@@ -51,120 +91,101 @@ async function executeToolMethod(method, toolName, rawArgs, context, hooks) {
|
|
|
51
91
|
input = result.data;
|
|
52
92
|
}
|
|
53
93
|
try {
|
|
54
|
-
const ctx = { params, input,
|
|
55
|
-
|
|
94
|
+
const ctx = { ...context, params, input, source: context.source };
|
|
95
|
+
if (lifecycle?.beforeHandle) {
|
|
96
|
+
await lifecycle.beforeHandle(ctx, method);
|
|
97
|
+
}
|
|
98
|
+
let data = await method.handler(ctx);
|
|
99
|
+
if (lifecycle?.afterHandle) {
|
|
100
|
+
const transformed = await lifecycle.afterHandle(ctx, data, method);
|
|
101
|
+
if (transformed !== undefined)
|
|
102
|
+
data = transformed;
|
|
103
|
+
}
|
|
104
|
+
if (method.outputSchema) {
|
|
105
|
+
const parsed = method.outputSchema.safeParse(data);
|
|
106
|
+
if (!parsed.success) {
|
|
107
|
+
return finish({
|
|
108
|
+
ok: false,
|
|
109
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
110
|
+
details: {
|
|
111
|
+
message: `Handler output does not match the contract: ${formatZodError(parsed.error)}`
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
data = parsed.data;
|
|
116
|
+
}
|
|
56
117
|
const output = data === undefined || data === null ? { status: "ok" } : data;
|
|
57
118
|
return finish({ ok: true, data: output });
|
|
58
119
|
} catch (err) {
|
|
59
|
-
|
|
60
|
-
return finish({
|
|
61
|
-
ok: false,
|
|
62
|
-
code: appErr.code,
|
|
63
|
-
details: appErr.details ?? { message: appErr.message },
|
|
64
|
-
...appErr.hint && { hint: appErr.hint }
|
|
65
|
-
});
|
|
120
|
+
return finish(toolResultFromError(err));
|
|
66
121
|
}
|
|
67
122
|
}
|
|
68
123
|
|
|
69
|
-
// src/tools/
|
|
70
|
-
|
|
71
|
-
"analytics",
|
|
72
|
-
"status",
|
|
73
|
-
"stats",
|
|
74
|
-
"settings",
|
|
75
|
-
"media",
|
|
76
|
-
"progress",
|
|
77
|
-
"news"
|
|
78
|
-
]);
|
|
79
|
-
function singularize(name) {
|
|
80
|
-
if (SINGULAR_EXCEPTIONS.has(name))
|
|
81
|
-
return name;
|
|
82
|
-
if (name.endsWith("ies"))
|
|
83
|
-
return `${name.slice(0, -3)}y`;
|
|
84
|
-
if (name.endsWith("s") && !name.endsWith("ss"))
|
|
85
|
-
return name.slice(0, -1);
|
|
86
|
-
return name;
|
|
87
|
-
}
|
|
88
|
-
function toToolName(serviceName, methodName) {
|
|
89
|
-
const normalized = serviceName.replace(/-/g, "_");
|
|
90
|
-
const singular = singularize(normalized);
|
|
91
|
-
if (methodName === "list")
|
|
92
|
-
return `list_${normalized}`;
|
|
93
|
-
if (methodName === "get")
|
|
94
|
-
return `get_${singular}`;
|
|
95
|
-
if (methodName === "create")
|
|
96
|
-
return `create_${singular}`;
|
|
97
|
-
if (methodName === "update")
|
|
98
|
-
return `update_${singular}`;
|
|
99
|
-
if (methodName === "delete")
|
|
100
|
-
return `delete_${singular}`;
|
|
101
|
-
const snake = methodName.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
102
|
-
return `${snake}_${singular}`.replace(/^_/, "");
|
|
103
|
-
}
|
|
124
|
+
// src/tools/mount.ts
|
|
125
|
+
import { z as z4 } from "zod";
|
|
104
126
|
|
|
105
|
-
// src/tools/
|
|
106
|
-
import { z } from "zod";
|
|
127
|
+
// src/tools/coerce.ts
|
|
128
|
+
import { z as z2 } from "zod";
|
|
107
129
|
function needsJsonCoercion(field) {
|
|
108
|
-
if (field instanceof
|
|
130
|
+
if (field instanceof z2.ZodArray || field instanceof z2.ZodObject)
|
|
109
131
|
return true;
|
|
110
|
-
if (field instanceof
|
|
132
|
+
if (field instanceof z2.ZodOptional || field instanceof z2.ZodNullable || field instanceof z2.ZodDefault) {
|
|
111
133
|
return needsJsonCoercion(field.unwrap());
|
|
112
134
|
}
|
|
113
135
|
return false;
|
|
114
136
|
}
|
|
115
137
|
var jsonCoerce = (val) => {
|
|
116
|
-
if (typeof val
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
if (typeof val !== "string")
|
|
139
|
+
return val;
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(val);
|
|
142
|
+
} catch {
|
|
143
|
+
return val;
|
|
122
144
|
}
|
|
123
|
-
return val;
|
|
124
145
|
};
|
|
125
146
|
function withJsonCoercion(schema) {
|
|
126
147
|
const shape = schema.shape;
|
|
127
148
|
const coerced = {};
|
|
128
|
-
for (const
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
for (const key of Object.keys(shape)) {
|
|
150
|
+
const field = shape[key];
|
|
151
|
+
if (!field || !needsJsonCoercion(field)) {
|
|
152
|
+
if (field)
|
|
153
|
+
coerced[key] = field;
|
|
131
154
|
continue;
|
|
132
155
|
}
|
|
133
156
|
let inner = field;
|
|
134
157
|
const wrappers = [];
|
|
135
|
-
while (inner instanceof
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
inner = inner.unwrap();
|
|
139
|
-
} else {
|
|
140
|
-
wrappers.push("nullable");
|
|
141
|
-
inner = inner.unwrap();
|
|
142
|
-
}
|
|
158
|
+
while (inner instanceof z2.ZodOptional || inner instanceof z2.ZodNullable) {
|
|
159
|
+
wrappers.push(inner instanceof z2.ZodOptional ? "optional" : "nullable");
|
|
160
|
+
inner = inner.unwrap();
|
|
143
161
|
}
|
|
144
|
-
let result =
|
|
162
|
+
let result = z2.preprocess(jsonCoerce, inner);
|
|
145
163
|
for (const wrapper of wrappers.reverse()) {
|
|
146
|
-
result = wrapper === "optional" ?
|
|
164
|
+
result = wrapper === "optional" ? z2.optional(result) : z2.nullable(result);
|
|
147
165
|
}
|
|
148
166
|
coerced[key] = result;
|
|
149
167
|
}
|
|
150
|
-
return
|
|
168
|
+
return z2.object(coerced);
|
|
151
169
|
}
|
|
170
|
+
|
|
171
|
+
// src/tools/flatten.ts
|
|
172
|
+
import { z as z3 } from "zod";
|
|
152
173
|
function flattenDiscriminatedUnion(union) {
|
|
153
174
|
const discriminator = union.def.discriminator;
|
|
154
175
|
const literalValues = [];
|
|
155
176
|
const fieldToVariants = new Map;
|
|
156
177
|
const fieldSchemas = {};
|
|
157
178
|
for (const opt of union.def.options) {
|
|
158
|
-
if (!(opt instanceof
|
|
159
|
-
throw new Error(`flattenDiscriminatedUnion:
|
|
179
|
+
if (!(opt instanceof z3.ZodObject)) {
|
|
180
|
+
throw new Error(`flattenDiscriminatedUnion: variant for discriminator '${discriminator}' is not a ZodObject`);
|
|
160
181
|
}
|
|
161
182
|
const discField = opt.shape[discriminator];
|
|
162
|
-
if (!(discField instanceof
|
|
183
|
+
if (!(discField instanceof z3.ZodLiteral)) {
|
|
163
184
|
throw new Error(`flattenDiscriminatedUnion: discriminator field '${discriminator}' must be z.literal()`);
|
|
164
185
|
}
|
|
165
186
|
const [literalValue] = discField.def.values;
|
|
166
187
|
if (typeof literalValue !== "string") {
|
|
167
|
-
throw new Error(
|
|
188
|
+
throw new Error("flattenDiscriminatedUnion: discriminator literal must be a string");
|
|
168
189
|
}
|
|
169
190
|
literalValues.push(literalValue);
|
|
170
191
|
for (const [key, field] of Object.entries(opt.shape)) {
|
|
@@ -174,7 +195,7 @@ function flattenDiscriminatedUnion(union) {
|
|
|
174
195
|
variants.push(literalValue);
|
|
175
196
|
fieldToVariants.set(key, variants);
|
|
176
197
|
if (!(key in fieldSchemas)) {
|
|
177
|
-
fieldSchemas[key] = field instanceof
|
|
198
|
+
fieldSchemas[key] = field instanceof z3.ZodOptional ? field : z3.optional(field);
|
|
178
199
|
}
|
|
179
200
|
}
|
|
180
201
|
}
|
|
@@ -183,32 +204,65 @@ function flattenDiscriminatedUnion(union) {
|
|
|
183
204
|
throw new Error("flattenDiscriminatedUnion: union has no options");
|
|
184
205
|
}
|
|
185
206
|
const shape = {
|
|
186
|
-
[discriminator]:
|
|
207
|
+
[discriminator]: z3.enum([firstLiteral, ...restLiterals])
|
|
187
208
|
};
|
|
188
209
|
for (const [key, field] of Object.entries(fieldSchemas)) {
|
|
189
210
|
const variants = fieldToVariants.get(key) ?? [];
|
|
190
211
|
const hint = `Required if ${discriminator} = ${variants.join(" | ")}`;
|
|
191
|
-
shape[key] = field instanceof
|
|
212
|
+
shape[key] = field instanceof z3.ZodType ? field.describe(hint) : field;
|
|
192
213
|
}
|
|
193
|
-
return
|
|
214
|
+
return z3.object(shape);
|
|
194
215
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
216
|
+
|
|
217
|
+
// src/tools/names.ts
|
|
218
|
+
var SINGULAR_EXCEPTIONS = new Set([
|
|
219
|
+
"analytics",
|
|
220
|
+
"status",
|
|
221
|
+
"stats",
|
|
222
|
+
"settings",
|
|
223
|
+
"media",
|
|
224
|
+
"progress",
|
|
225
|
+
"news"
|
|
226
|
+
]);
|
|
227
|
+
function singularize(name) {
|
|
228
|
+
if (SINGULAR_EXCEPTIONS.has(name))
|
|
229
|
+
return name;
|
|
230
|
+
if (name.endsWith("ies"))
|
|
231
|
+
return `${name.slice(0, -3)}y`;
|
|
232
|
+
if (name.endsWith("s") && !name.endsWith("ss"))
|
|
233
|
+
return name.slice(0, -1);
|
|
234
|
+
return name;
|
|
235
|
+
}
|
|
236
|
+
function toToolName(serviceName, methodName) {
|
|
237
|
+
const normalized = serviceName.replace(/-/g, "_");
|
|
238
|
+
const singular = singularize(normalized);
|
|
239
|
+
if (methodName === "list")
|
|
240
|
+
return `list_${normalized}`;
|
|
241
|
+
if (methodName === "get")
|
|
242
|
+
return `get_${singular}`;
|
|
243
|
+
if (methodName === "create")
|
|
244
|
+
return `create_${singular}`;
|
|
245
|
+
if (methodName === "update")
|
|
246
|
+
return `update_${singular}`;
|
|
247
|
+
if (methodName === "delete")
|
|
248
|
+
return `delete_${singular}`;
|
|
249
|
+
const snake = methodName.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
250
|
+
return `${snake}_${singular}`.replace(/^_/, "");
|
|
208
251
|
}
|
|
209
252
|
|
|
210
253
|
// src/tools/mount.ts
|
|
211
|
-
function
|
|
254
|
+
function applyExtend(base, extra) {
|
|
255
|
+
if (base instanceof z4.ZodObject) {
|
|
256
|
+
const conflicts = Object.keys(extra).filter((key) => (key in base.shape));
|
|
257
|
+
if (conflicts.length > 0) {
|
|
258
|
+
throw new Error(`Tool extend conflict: ${conflicts.join(", ")} already declared by the contract`);
|
|
259
|
+
}
|
|
260
|
+
return z4.object({ ...extra, ...base.shape });
|
|
261
|
+
}
|
|
262
|
+
return z4.intersection(z4.object(extra), base);
|
|
263
|
+
}
|
|
264
|
+
function collectTools(service, transport, config = {}) {
|
|
265
|
+
const { extend, coerceJsonArgs = true, flattenUnionInput = false } = config;
|
|
212
266
|
const tools = [];
|
|
213
267
|
for (const [methodName, method] of Object.entries(service.methods)) {
|
|
214
268
|
if (method.expose && !method.expose.includes(transport))
|
|
@@ -216,9 +270,15 @@ function collectTools(service, transport, extend) {
|
|
|
216
270
|
if (method.multipart)
|
|
217
271
|
continue;
|
|
218
272
|
const name = method.toolName ?? toToolName(service.name, methodName);
|
|
219
|
-
|
|
273
|
+
let baseSchema = mergeSchemas(method.paramsSchema, method.inputSchema);
|
|
274
|
+
if (flattenUnionInput && baseSchema instanceof z4.ZodDiscriminatedUnion) {
|
|
275
|
+
baseSchema = flattenDiscriminatedUnion(baseSchema);
|
|
276
|
+
}
|
|
277
|
+
if (coerceJsonArgs && baseSchema instanceof z4.ZodObject) {
|
|
278
|
+
baseSchema = withJsonCoercion(baseSchema);
|
|
279
|
+
}
|
|
220
280
|
const shouldExtend = !!extend && (!extend.filter || extend.filter(service, method));
|
|
221
|
-
const schema = shouldExtend ?
|
|
281
|
+
const schema = shouldExtend && extend ? applyExtend(baseSchema, extend.schema) : baseSchema;
|
|
222
282
|
tools.push({ method, name, schema, shouldExtend });
|
|
223
283
|
}
|
|
224
284
|
return tools;
|
|
@@ -231,111 +291,226 @@ function createToolRunner(config) {
|
|
|
231
291
|
extraContext = await config.extend.resolve(rawArgs);
|
|
232
292
|
}
|
|
233
293
|
const cleanArgs = extendKeys ? Object.fromEntries(Object.entries(rawArgs).filter(([key]) => !extendKeys.has(key))) : rawArgs;
|
|
234
|
-
return executeToolMethod(tool.method, tool.name, cleanArgs, {
|
|
294
|
+
return executeToolMethod(tool.method, tool.name, cleanArgs, { ...config.context, ...extraContext, source: config.source }, config.hooks, config.lifecycle);
|
|
235
295
|
};
|
|
236
296
|
}
|
|
237
|
-
function formatToolError(result) {
|
|
297
|
+
function formatToolError(result, toolName, errorHint) {
|
|
238
298
|
const err = { error: result.code };
|
|
239
299
|
if (result.details)
|
|
240
300
|
err.details = result.details;
|
|
301
|
+
const hints = [];
|
|
241
302
|
if (result.hint)
|
|
242
|
-
|
|
303
|
+
hints.push(result.hint);
|
|
304
|
+
if (errorHint && toolName) {
|
|
305
|
+
const global = errorHint(toolName, result.code);
|
|
306
|
+
if (global)
|
|
307
|
+
hints.push(global);
|
|
308
|
+
}
|
|
309
|
+
if (hints.length > 0)
|
|
310
|
+
err._hint = hints.join(" ");
|
|
243
311
|
return err;
|
|
244
312
|
}
|
|
245
313
|
|
|
246
314
|
// src/tools/agent.ts
|
|
247
|
-
function
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
function mountAgent(service, config = {}) {
|
|
315
|
+
function mountAgent(services, config = {}) {
|
|
316
|
+
const serviceList = Array.isArray(services) ? services : [services];
|
|
251
317
|
const tools = {};
|
|
252
318
|
const runTool = createToolRunner({
|
|
253
319
|
source: "agent",
|
|
254
320
|
extend: config.extend,
|
|
255
321
|
context: config.context,
|
|
256
|
-
hooks: config.hooks
|
|
322
|
+
hooks: config.hooks,
|
|
323
|
+
lifecycle: config.lifecycle,
|
|
324
|
+
errorHint: config.errorHint
|
|
257
325
|
});
|
|
258
|
-
for (const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
} catch (err) {
|
|
267
|
-
const appErr = normalizeError(err);
|
|
268
|
-
return { error: appErr.code, details: appErr.details };
|
|
269
|
-
}
|
|
326
|
+
for (const service of serviceList) {
|
|
327
|
+
for (const mountable of collectTools(service, "AGENT", {
|
|
328
|
+
extend: config.extend,
|
|
329
|
+
coerceJsonArgs: config.coerceJsonArgs,
|
|
330
|
+
flattenUnionInput: config.flattenUnionInput
|
|
331
|
+
})) {
|
|
332
|
+
if (mountable.name in tools) {
|
|
333
|
+
throw new Error(`Duplicate agent tool name "${mountable.name}" across mounted services`);
|
|
270
334
|
}
|
|
271
|
-
|
|
335
|
+
tools[mountable.name] = tool({
|
|
336
|
+
description: mountable.method.desc,
|
|
337
|
+
inputSchema: zodSchema(mountable.schema),
|
|
338
|
+
execute: async (rawArgs) => {
|
|
339
|
+
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
340
|
+
try {
|
|
341
|
+
const result = await runTool(mountable, args);
|
|
342
|
+
if (result.ok)
|
|
343
|
+
return result.data;
|
|
344
|
+
return formatToolError(result, mountable.name, config.errorHint);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
return formatToolError(toolResultFromError(err), mountable.name, config.errorHint);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
272
351
|
}
|
|
273
352
|
return tools;
|
|
274
353
|
}
|
|
354
|
+
// src/tools/json-schema.ts
|
|
355
|
+
import { z as z5 } from "zod";
|
|
356
|
+
function toJsonSchema(schema, io) {
|
|
357
|
+
return z5.toJSONSchema(schema, {
|
|
358
|
+
io,
|
|
359
|
+
target: "draft-2020-12",
|
|
360
|
+
unrepresentable: "throw",
|
|
361
|
+
cycles: "ref"
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/tools/manifest.ts
|
|
366
|
+
function buildToolManifest(tools) {
|
|
367
|
+
return tools.map((t) => ({
|
|
368
|
+
name: t.name,
|
|
369
|
+
description: t.method.desc,
|
|
370
|
+
inputSchema: toJsonSchema(t.schema, "input")
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
275
373
|
// src/tools/mcp.ts
|
|
276
374
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
277
|
-
import { z as
|
|
375
|
+
import { z as z6 } from "zod";
|
|
278
376
|
function textBlock(text) {
|
|
279
377
|
return [{ type: "text", text }];
|
|
280
378
|
}
|
|
281
|
-
function formatMcpResult(result,
|
|
379
|
+
function formatMcpResult(result, mode, toolName, errorHint) {
|
|
282
380
|
if (result.ok) {
|
|
283
381
|
const content = textBlock(JSON.stringify(result.data, null, 2));
|
|
284
|
-
if (
|
|
382
|
+
if (mode === "wrapped") {
|
|
383
|
+
return { content, structuredContent: { result: result.data } };
|
|
384
|
+
}
|
|
385
|
+
if (mode === "direct" && isRecord(result.data)) {
|
|
285
386
|
return { content, structuredContent: result.data };
|
|
286
387
|
}
|
|
287
388
|
return { content };
|
|
288
389
|
}
|
|
289
390
|
return {
|
|
290
|
-
content: textBlock(JSON.stringify(formatToolError(result), null, 2)),
|
|
391
|
+
content: textBlock(JSON.stringify(formatToolError(result, toolName, errorHint), null, 2)),
|
|
291
392
|
isError: true
|
|
292
393
|
};
|
|
293
394
|
}
|
|
395
|
+
function probeSchema(schema, io) {
|
|
396
|
+
try {
|
|
397
|
+
toJsonSchema(schema, io);
|
|
398
|
+
return null;
|
|
399
|
+
} catch (err) {
|
|
400
|
+
return err instanceof Error ? err.message : String(err);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function resolveOutputSchema(outputSchema) {
|
|
404
|
+
if (!outputSchema)
|
|
405
|
+
return null;
|
|
406
|
+
if (outputSchema instanceof z6.ZodObject) {
|
|
407
|
+
return { schema: outputSchema, mode: "direct" };
|
|
408
|
+
}
|
|
409
|
+
return { schema: z6.object({ result: outputSchema }), mode: "wrapped" };
|
|
410
|
+
}
|
|
411
|
+
function reportIncompatible(message, policy, logger, failures) {
|
|
412
|
+
if (policy === "throw") {
|
|
413
|
+
failures.push(message);
|
|
414
|
+
} else if (policy === "warn") {
|
|
415
|
+
if (logger)
|
|
416
|
+
logger.warn(`[stitchkit] ${message}`);
|
|
417
|
+
else
|
|
418
|
+
console.warn(`[stitchkit] ${message}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function throwIfFailures(failures) {
|
|
422
|
+
if (failures.length > 0) {
|
|
423
|
+
throw new Error(`[stitchkit] ${failures.length} MCP tool(s) have an incompatible schema:
|
|
424
|
+
- ${failures.join(`
|
|
425
|
+
- `)}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function prepareMcpTool(mountable, policy, logger, failures, seen) {
|
|
429
|
+
if (seen.has(mountable.name)) {
|
|
430
|
+
throw new Error(`Duplicate MCP tool name "${mountable.name}" across mounted services`);
|
|
431
|
+
}
|
|
432
|
+
seen.add(mountable.name);
|
|
433
|
+
if (!(mountable.schema instanceof z6.ZodObject)) {
|
|
434
|
+
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);
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
const inputError = probeSchema(mountable.schema, "input");
|
|
438
|
+
if (inputError) {
|
|
439
|
+
reportIncompatible(`MCP tool "${mountable.name}" — input schema is not JSON Schema-compatible: ${inputError}`, policy, logger, failures);
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
const resolved = resolveOutputSchema(mountable.method.outputSchema);
|
|
443
|
+
if (!resolved)
|
|
444
|
+
return { outputMode: "none" };
|
|
445
|
+
const outputError = probeSchema(resolved.schema, "output");
|
|
446
|
+
if (outputError) {
|
|
447
|
+
reportIncompatible(`MCP tool "${mountable.name}" — output schema is not JSON Schema-compatible: ${outputError}`, policy, logger, failures);
|
|
448
|
+
return { outputMode: "none" };
|
|
449
|
+
}
|
|
450
|
+
return { outputSchema: resolved.schema, outputMode: resolved.mode };
|
|
451
|
+
}
|
|
452
|
+
function validateMcpSchemas(services, onIncompatibleSchema = "throw", logger) {
|
|
453
|
+
const seen = new Set;
|
|
454
|
+
const failures = [];
|
|
455
|
+
for (const service of services) {
|
|
456
|
+
for (const mountable of collectTools(service, "MCP", undefined)) {
|
|
457
|
+
prepareMcpTool(mountable, onIncompatibleSchema, logger, failures, seen);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
throwIfFailures(failures);
|
|
461
|
+
}
|
|
294
462
|
function mountMcp(mcpServer, services, config = {}) {
|
|
295
463
|
const serviceList = Array.isArray(services) ? services : [services];
|
|
464
|
+
const policy = config.onIncompatibleSchema ?? "throw";
|
|
296
465
|
const runTool = createToolRunner({
|
|
297
466
|
source: "mcp",
|
|
298
467
|
extend: config.extend,
|
|
299
468
|
context: config.context,
|
|
300
|
-
hooks: config.hooks
|
|
469
|
+
hooks: config.hooks,
|
|
470
|
+
lifecycle: config.lifecycle,
|
|
471
|
+
errorHint: config.errorHint
|
|
301
472
|
});
|
|
473
|
+
const seen = new Set;
|
|
474
|
+
const failures = [];
|
|
302
475
|
for (const service of serviceList) {
|
|
303
|
-
for (const mountable of collectTools(service, "MCP",
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
476
|
+
for (const mountable of collectTools(service, "MCP", {
|
|
477
|
+
extend: config.extend,
|
|
478
|
+
coerceJsonArgs: config.coerceJsonArgs,
|
|
479
|
+
flattenUnionInput: config.flattenUnionInput
|
|
480
|
+
})) {
|
|
481
|
+
const prepared = prepareMcpTool(mountable, policy, config.logger, failures, seen);
|
|
482
|
+
if (!prepared)
|
|
308
483
|
continue;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
try {
|
|
313
|
-
z3.toJSONSchema(mountable.method.outputSchema);
|
|
314
|
-
outputSchema = mountable.method.outputSchema;
|
|
315
|
-
} catch {
|
|
316
|
-
outputSchema = undefined;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
const toolConfig = { description: mountable.method.desc, inputSchema: mountable.schema.shape };
|
|
320
|
-
if (outputSchema)
|
|
321
|
-
toolConfig.outputSchema = outputSchema.shape;
|
|
484
|
+
const toolConfig = { description: mountable.method.desc, inputSchema: mountable.schema };
|
|
485
|
+
if (prepared.outputSchema)
|
|
486
|
+
toolConfig.outputSchema = prepared.outputSchema;
|
|
322
487
|
mcpServer.registerTool(mountable.name, toolConfig, async (rawArgs) => {
|
|
488
|
+
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
323
489
|
try {
|
|
324
|
-
const result = await runTool(mountable,
|
|
325
|
-
return formatMcpResult(result,
|
|
490
|
+
const result = await runTool(mountable, args);
|
|
491
|
+
return formatMcpResult(result, prepared.outputMode, mountable.name, config.errorHint);
|
|
326
492
|
} catch (err) {
|
|
327
|
-
|
|
328
|
-
return formatMcpResult({ ok: false, code: appErr.code, details: appErr.details }, false);
|
|
493
|
+
return formatMcpResult(toolResultFromError(err), "none", mountable.name, config.errorHint);
|
|
329
494
|
}
|
|
330
495
|
});
|
|
331
496
|
}
|
|
332
497
|
}
|
|
498
|
+
throwIfFailures(failures);
|
|
333
499
|
}
|
|
334
500
|
function buildMcpServer(config, auth) {
|
|
335
501
|
const server = new McpServer(config.serverInfo, config.instructions ? { instructions: config.instructions } : undefined);
|
|
336
502
|
const services = typeof config.services === "function" ? config.services(auth) : config.services;
|
|
337
503
|
const context = config.context?.(auth);
|
|
338
|
-
mountMcp(server, services, {
|
|
504
|
+
mountMcp(server, services, {
|
|
505
|
+
context,
|
|
506
|
+
hooks: config.hooks,
|
|
507
|
+
lifecycle: config.lifecycle,
|
|
508
|
+
onIncompatibleSchema: config.onIncompatibleSchema,
|
|
509
|
+
logger: config.logger,
|
|
510
|
+
coerceJsonArgs: config.coerceJsonArgs,
|
|
511
|
+
flattenUnionInput: config.flattenUnionInput,
|
|
512
|
+
errorHint: config.errorHint
|
|
513
|
+
});
|
|
339
514
|
config.nativeTools?.(server);
|
|
340
515
|
return server;
|
|
341
516
|
}
|
|
@@ -382,6 +557,9 @@ function jsonRpcError(code, message, status) {
|
|
|
382
557
|
return Response.json({ jsonrpc: "2.0", error: { code, message }, id: null }, { status });
|
|
383
558
|
}
|
|
384
559
|
function createMcpHandler(config) {
|
|
560
|
+
if (Array.isArray(config.services)) {
|
|
561
|
+
validateMcpSchemas(config.services, config.onIncompatibleSchema, config.logger);
|
|
562
|
+
}
|
|
385
563
|
const eventStore = new InMemoryEventStore;
|
|
386
564
|
const sessions = new Map;
|
|
387
565
|
setInterval(() => {
|
|
@@ -481,7 +659,7 @@ import { lookup } from "node:dns/promises";
|
|
|
481
659
|
import { readFile, stat } from "node:fs/promises";
|
|
482
660
|
import { isIP } from "node:net";
|
|
483
661
|
import { extname, resolve, sep } from "node:path";
|
|
484
|
-
import { z as
|
|
662
|
+
import { z as z7 } from "zod";
|
|
485
663
|
var MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
486
664
|
var EXT_MIME = {
|
|
487
665
|
".png": "image/png",
|
|
@@ -624,7 +802,7 @@ function mountViewFile(server, options = {}) {
|
|
|
624
802
|
server.registerTool("view_file", {
|
|
625
803
|
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.",
|
|
626
804
|
inputSchema: {
|
|
627
|
-
paths:
|
|
805
|
+
paths: z7.union([z7.string(), z7.array(z7.string())]).describe("Media URL(s) or file path(s) to view")
|
|
628
806
|
}
|
|
629
807
|
}, async (args) => {
|
|
630
808
|
const list = Array.isArray(args.paths) ? args.paths : [args.paths];
|
|
@@ -643,12 +821,17 @@ function mountViewFile(server, options = {}) {
|
|
|
643
821
|
});
|
|
644
822
|
}
|
|
645
823
|
export {
|
|
824
|
+
withJsonCoercion,
|
|
825
|
+
validateMcpSchemas,
|
|
646
826
|
resolveMedia,
|
|
647
827
|
mountViewFile,
|
|
648
828
|
mountMcp,
|
|
649
829
|
mountAgent,
|
|
650
830
|
implementRemote,
|
|
831
|
+
flattenDiscriminatedUnion,
|
|
651
832
|
createStdioMcpServer,
|
|
652
833
|
createMcpHandler,
|
|
834
|
+
collectTools,
|
|
835
|
+
buildToolManifest,
|
|
653
836
|
buildMcpServer
|
|
654
837
|
};
|