stitchkit 0.0.1 → 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/LICENSE +21 -0
- package/README.md +376 -4
- package/dist/browser/client.d.ts +30 -0
- package/dist/browser/client.d.ts.map +1 -0
- package/dist/browser/http.d.ts +77 -0
- package/dist/browser/http.d.ts.map +1 -0
- package/dist/browser/socket-io.d.ts +69 -0
- package/dist/browser/socket-io.d.ts.map +1 -0
- package/dist/browser/stream.d.ts +2 -0
- package/dist/browser/stream.d.ts.map +1 -0
- package/dist/contract/define.d.ts +94 -0
- package/dist/contract/define.d.ts.map +1 -0
- package/dist/contract/errors.d.ts +47 -0
- package/dist/contract/errors.d.ts.map +1 -0
- package/dist/contract/index.d.ts +4 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +26 -0
- package/dist/contract/pagination.d.ts +19 -0
- package/dist/contract/pagination.d.ts.map +1 -0
- package/dist/index-5sxnvwb1.js +368 -0
- package/dist/index-809wc1tt.js +18 -0
- package/dist/index-a35v22fh.js +29 -0
- package/dist/index-kckky6zw.js +96 -0
- package/dist/index-ke4mx4ea.js +38 -0
- package/dist/index-n7bmdwmz.js +68 -0
- package/dist/index-v2z2v3mq.js +587 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +111 -0
- package/dist/internal/errors.d.ts +5 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/typed.d.ts +9 -0
- package/dist/internal/typed.d.ts.map +1 -0
- package/dist/node.d.ts +4 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +26 -0
- package/dist/observability/audit.d.ts +32 -0
- package/dist/observability/audit.d.ts.map +1 -0
- package/dist/observability/context.d.ts +60 -0
- package/dist/observability/context.d.ts.map +1 -0
- package/dist/observability/event.d.ts +49 -0
- package/dist/observability/event.d.ts.map +1 -0
- package/dist/observability/index.d.ts +14 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +266 -0
- package/dist/observability/sanitize.d.ts +42 -0
- package/dist/observability/sanitize.d.ts.map +1 -0
- package/dist/observability/trace.d.ts +42 -0
- package/dist/observability/trace.d.ts.map +1 -0
- package/dist/react/cache-bridge.d.ts +53 -0
- package/dist/react/cache-bridge.d.ts.map +1 -0
- package/dist/react/cursor-query.d.ts +37 -0
- package/dist/react/cursor-query.d.ts.map +1 -0
- package/dist/react.d.ts +3 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +52 -0
- package/dist/server/cache.d.ts +15 -0
- package/dist/server/cache.d.ts.map +1 -0
- package/dist/server/context.d.ts +9 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/create.d.ts +4 -0
- package/dist/server/create.d.ts.map +1 -0
- package/dist/server/event-bus.d.ts +24 -0
- package/dist/server/event-bus.d.ts.map +1 -0
- package/dist/server/implement.d.ts +16 -0
- package/dist/server/implement.d.ts.map +1 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +399 -0
- package/dist/server/logger.d.ts +21 -0
- package/dist/server/logger.d.ts.map +1 -0
- package/dist/server/middleware/auth.d.ts +50 -0
- package/dist/server/middleware/auth.d.ts.map +1 -0
- package/dist/server/middleware/cookies.d.ts +34 -0
- package/dist/server/middleware/cookies.d.ts.map +1 -0
- package/dist/server/middleware/cors.d.ts +9 -0
- package/dist/server/middleware/cors.d.ts.map +1 -0
- package/dist/server/multipart.d.ts +14 -0
- package/dist/server/multipart.d.ts.map +1 -0
- package/dist/server/node.d.ts +12 -0
- package/dist/server/node.d.ts.map +1 -0
- package/dist/server/rate-limit.d.ts +17 -0
- package/dist/server/rate-limit.d.ts.map +1 -0
- package/dist/server/request.d.ts +22 -0
- package/dist/server/request.d.ts.map +1 -0
- package/dist/server/router.d.ts +42 -0
- package/dist/server/router.d.ts.map +1 -0
- package/dist/server/socket-io.d.ts +42 -0
- package/dist/server/socket-io.d.ts.map +1 -0
- package/dist/server/stream.d.ts +18 -0
- package/dist/server/stream.d.ts.map +1 -0
- package/dist/server/swept-map.d.ts +22 -0
- package/dist/server/swept-map.d.ts.map +1 -0
- package/dist/server/types.d.ts +127 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/tools/agent.d.ts +27 -0
- package/dist/tools/agent.d.ts.map +1 -0
- package/dist/tools/coerce.d.ts +8 -0
- package/dist/tools/coerce.d.ts.map +1 -0
- package/dist/tools/execute.d.ts +45 -0
- package/dist/tools/execute.d.ts.map +1 -0
- 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 +21 -0
- package/dist/tools/mcp-handler.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.d.ts +25 -0
- package/dist/tools/mcp-stdio.d.ts.map +1 -0
- package/dist/tools/mcp.d.ts +92 -0
- package/dist/tools/mcp.d.ts.map +1 -0
- package/dist/tools/mount.d.ts +73 -0
- package/dist/tools/mount.d.ts.map +1 -0
- package/dist/tools/names.d.ts +2 -0
- package/dist/tools/names.d.ts.map +1 -0
- package/dist/tools/remote.d.ts +22 -0
- package/dist/tools/remote.d.ts.map +1 -0
- package/dist/tools/schema.d.ts +21 -0
- package/dist/tools/schema.d.ts.map +1 -0
- package/dist/tools/view-file.d.ts +57 -0
- package/dist/tools/view-file.d.ts.map +1 -0
- package/dist/tools.d.ts +12 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +837 -0
- package/package.json +147 -4
package/dist/tools.js
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createClient
|
|
3
|
+
} from "./index-5sxnvwb1.js";
|
|
4
|
+
import {
|
|
5
|
+
isRecord
|
|
6
|
+
} from "./index-809wc1tt.js";
|
|
7
|
+
import {
|
|
8
|
+
formatZodError,
|
|
9
|
+
normalizeError
|
|
10
|
+
} from "./index-a35v22fh.js";
|
|
11
|
+
import"./index-kckky6zw.js";
|
|
12
|
+
|
|
13
|
+
// src/tools/agent.ts
|
|
14
|
+
import { tool, zodSchema } from "ai";
|
|
15
|
+
|
|
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
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/tools/execute.ts
|
|
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) {
|
|
52
|
+
const startedAt = Date.now();
|
|
53
|
+
const finish = async (result) => {
|
|
54
|
+
await hooks?.afterToolCall?.(toolName, rawArgs, result, Date.now() - startedAt, context);
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
if (hooks?.beforeToolCall) {
|
|
58
|
+
await hooks.beforeToolCall(toolName, rawArgs, context);
|
|
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
|
+
}
|
|
69
|
+
let params;
|
|
70
|
+
if (method.paramsSchema) {
|
|
71
|
+
const result = method.paramsSchema.safeParse(paramArgs);
|
|
72
|
+
if (!result.success) {
|
|
73
|
+
return finish({
|
|
74
|
+
ok: false,
|
|
75
|
+
code: "VALIDATION_ERROR",
|
|
76
|
+
details: { message: `Invalid params: ${formatZodError(result.error)}` }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
params = result.data;
|
|
80
|
+
}
|
|
81
|
+
let input;
|
|
82
|
+
if (method.inputSchema) {
|
|
83
|
+
const result = method.inputSchema.safeParse(inputArgs);
|
|
84
|
+
if (!result.success) {
|
|
85
|
+
return finish({
|
|
86
|
+
ok: false,
|
|
87
|
+
code: "VALIDATION_ERROR",
|
|
88
|
+
details: { message: `Invalid input: ${formatZodError(result.error)}` }
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
input = result.data;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
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
|
+
}
|
|
117
|
+
const output = data === undefined || data === null ? { status: "ok" } : data;
|
|
118
|
+
return finish({ ok: true, data: output });
|
|
119
|
+
} catch (err) {
|
|
120
|
+
return finish(toolResultFromError(err));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/tools/mount.ts
|
|
125
|
+
import { z as z4 } from "zod";
|
|
126
|
+
|
|
127
|
+
// src/tools/coerce.ts
|
|
128
|
+
import { z as z2 } from "zod";
|
|
129
|
+
function needsJsonCoercion(field) {
|
|
130
|
+
if (field instanceof z2.ZodArray || field instanceof z2.ZodObject)
|
|
131
|
+
return true;
|
|
132
|
+
if (field instanceof z2.ZodOptional || field instanceof z2.ZodNullable || field instanceof z2.ZodDefault) {
|
|
133
|
+
return needsJsonCoercion(field.unwrap());
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
var jsonCoerce = (val) => {
|
|
138
|
+
if (typeof val !== "string")
|
|
139
|
+
return val;
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(val);
|
|
142
|
+
} catch {
|
|
143
|
+
return val;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
function withJsonCoercion(schema) {
|
|
147
|
+
const shape = schema.shape;
|
|
148
|
+
const coerced = {};
|
|
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;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
let inner = field;
|
|
157
|
+
const wrappers = [];
|
|
158
|
+
while (inner instanceof z2.ZodOptional || inner instanceof z2.ZodNullable) {
|
|
159
|
+
wrappers.push(inner instanceof z2.ZodOptional ? "optional" : "nullable");
|
|
160
|
+
inner = inner.unwrap();
|
|
161
|
+
}
|
|
162
|
+
let result = z2.preprocess(jsonCoerce, inner);
|
|
163
|
+
for (const wrapper of wrappers.reverse()) {
|
|
164
|
+
result = wrapper === "optional" ? z2.optional(result) : z2.nullable(result);
|
|
165
|
+
}
|
|
166
|
+
coerced[key] = result;
|
|
167
|
+
}
|
|
168
|
+
return z2.object(coerced);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/tools/flatten.ts
|
|
172
|
+
import { z as z3 } from "zod";
|
|
173
|
+
function flattenDiscriminatedUnion(union) {
|
|
174
|
+
const discriminator = union.def.discriminator;
|
|
175
|
+
const literalValues = [];
|
|
176
|
+
const fieldToVariants = new Map;
|
|
177
|
+
const fieldSchemas = {};
|
|
178
|
+
for (const opt of union.def.options) {
|
|
179
|
+
if (!(opt instanceof z3.ZodObject)) {
|
|
180
|
+
throw new Error(`flattenDiscriminatedUnion: variant for discriminator '${discriminator}' is not a ZodObject`);
|
|
181
|
+
}
|
|
182
|
+
const discField = opt.shape[discriminator];
|
|
183
|
+
if (!(discField instanceof z3.ZodLiteral)) {
|
|
184
|
+
throw new Error(`flattenDiscriminatedUnion: discriminator field '${discriminator}' must be z.literal()`);
|
|
185
|
+
}
|
|
186
|
+
const [literalValue] = discField.def.values;
|
|
187
|
+
if (typeof literalValue !== "string") {
|
|
188
|
+
throw new Error("flattenDiscriminatedUnion: discriminator literal must be a string");
|
|
189
|
+
}
|
|
190
|
+
literalValues.push(literalValue);
|
|
191
|
+
for (const [key, field] of Object.entries(opt.shape)) {
|
|
192
|
+
if (key === discriminator)
|
|
193
|
+
continue;
|
|
194
|
+
const variants = fieldToVariants.get(key) ?? [];
|
|
195
|
+
variants.push(literalValue);
|
|
196
|
+
fieldToVariants.set(key, variants);
|
|
197
|
+
if (!(key in fieldSchemas)) {
|
|
198
|
+
fieldSchemas[key] = field instanceof z3.ZodOptional ? field : z3.optional(field);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const [firstLiteral, ...restLiterals] = literalValues;
|
|
203
|
+
if (!firstLiteral) {
|
|
204
|
+
throw new Error("flattenDiscriminatedUnion: union has no options");
|
|
205
|
+
}
|
|
206
|
+
const shape = {
|
|
207
|
+
[discriminator]: z3.enum([firstLiteral, ...restLiterals])
|
|
208
|
+
};
|
|
209
|
+
for (const [key, field] of Object.entries(fieldSchemas)) {
|
|
210
|
+
const variants = fieldToVariants.get(key) ?? [];
|
|
211
|
+
const hint = `Required if ${discriminator} = ${variants.join(" | ")}`;
|
|
212
|
+
shape[key] = field instanceof z3.ZodType ? field.describe(hint) : field;
|
|
213
|
+
}
|
|
214
|
+
return z3.object(shape);
|
|
215
|
+
}
|
|
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(/^_/, "");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/tools/mount.ts
|
|
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;
|
|
266
|
+
const tools = [];
|
|
267
|
+
for (const [methodName, method] of Object.entries(service.methods)) {
|
|
268
|
+
if (method.expose && !method.expose.includes(transport))
|
|
269
|
+
continue;
|
|
270
|
+
if (method.multipart)
|
|
271
|
+
continue;
|
|
272
|
+
const name = method.toolName ?? toToolName(service.name, methodName);
|
|
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
|
+
}
|
|
280
|
+
const shouldExtend = !!extend && (!extend.filter || extend.filter(service, method));
|
|
281
|
+
const schema = shouldExtend && extend ? applyExtend(baseSchema, extend.schema) : baseSchema;
|
|
282
|
+
tools.push({ method, name, schema, shouldExtend });
|
|
283
|
+
}
|
|
284
|
+
return tools;
|
|
285
|
+
}
|
|
286
|
+
function createToolRunner(config) {
|
|
287
|
+
const extendKeys = config.extend ? new Set(Object.keys(config.extend.schema)) : null;
|
|
288
|
+
return async (tool, rawArgs) => {
|
|
289
|
+
let extraContext = {};
|
|
290
|
+
if (tool.shouldExtend && config.extend) {
|
|
291
|
+
extraContext = await config.extend.resolve(rawArgs);
|
|
292
|
+
}
|
|
293
|
+
const cleanArgs = extendKeys ? Object.fromEntries(Object.entries(rawArgs).filter(([key]) => !extendKeys.has(key))) : rawArgs;
|
|
294
|
+
return executeToolMethod(tool.method, tool.name, cleanArgs, { ...config.context, ...extraContext, source: config.source }, config.hooks, config.lifecycle);
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function formatToolError(result, toolName, errorHint) {
|
|
298
|
+
const err = { error: result.code };
|
|
299
|
+
if (result.details)
|
|
300
|
+
err.details = result.details;
|
|
301
|
+
const hints = [];
|
|
302
|
+
if (result.hint)
|
|
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(" ");
|
|
311
|
+
return err;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/tools/agent.ts
|
|
315
|
+
function mountAgent(services, config = {}) {
|
|
316
|
+
const serviceList = Array.isArray(services) ? services : [services];
|
|
317
|
+
const tools = {};
|
|
318
|
+
const runTool = createToolRunner({
|
|
319
|
+
source: "agent",
|
|
320
|
+
extend: config.extend,
|
|
321
|
+
context: config.context,
|
|
322
|
+
hooks: config.hooks,
|
|
323
|
+
lifecycle: config.lifecycle,
|
|
324
|
+
errorHint: config.errorHint
|
|
325
|
+
});
|
|
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`);
|
|
334
|
+
}
|
|
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
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return tools;
|
|
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
|
+
}
|
|
373
|
+
// src/tools/mcp.ts
|
|
374
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
375
|
+
import { z as z6 } from "zod";
|
|
376
|
+
function textBlock(text) {
|
|
377
|
+
return [{ type: "text", text }];
|
|
378
|
+
}
|
|
379
|
+
function formatMcpResult(result, mode, toolName, errorHint) {
|
|
380
|
+
if (result.ok) {
|
|
381
|
+
const content = textBlock(JSON.stringify(result.data, null, 2));
|
|
382
|
+
if (mode === "wrapped") {
|
|
383
|
+
return { content, structuredContent: { result: result.data } };
|
|
384
|
+
}
|
|
385
|
+
if (mode === "direct" && isRecord(result.data)) {
|
|
386
|
+
return { content, structuredContent: result.data };
|
|
387
|
+
}
|
|
388
|
+
return { content };
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
content: textBlock(JSON.stringify(formatToolError(result, toolName, errorHint), null, 2)),
|
|
392
|
+
isError: true
|
|
393
|
+
};
|
|
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
|
+
}
|
|
462
|
+
function mountMcp(mcpServer, services, config = {}) {
|
|
463
|
+
const serviceList = Array.isArray(services) ? services : [services];
|
|
464
|
+
const policy = config.onIncompatibleSchema ?? "throw";
|
|
465
|
+
const runTool = createToolRunner({
|
|
466
|
+
source: "mcp",
|
|
467
|
+
extend: config.extend,
|
|
468
|
+
context: config.context,
|
|
469
|
+
hooks: config.hooks,
|
|
470
|
+
lifecycle: config.lifecycle,
|
|
471
|
+
errorHint: config.errorHint
|
|
472
|
+
});
|
|
473
|
+
const seen = new Set;
|
|
474
|
+
const failures = [];
|
|
475
|
+
for (const service of serviceList) {
|
|
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)
|
|
483
|
+
continue;
|
|
484
|
+
const toolConfig = { description: mountable.method.desc, inputSchema: mountable.schema };
|
|
485
|
+
if (prepared.outputSchema)
|
|
486
|
+
toolConfig.outputSchema = prepared.outputSchema;
|
|
487
|
+
mcpServer.registerTool(mountable.name, toolConfig, async (rawArgs) => {
|
|
488
|
+
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
489
|
+
try {
|
|
490
|
+
const result = await runTool(mountable, args);
|
|
491
|
+
return formatMcpResult(result, prepared.outputMode, mountable.name, config.errorHint);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
return formatMcpResult(toolResultFromError(err), "none", mountable.name, config.errorHint);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
throwIfFailures(failures);
|
|
499
|
+
}
|
|
500
|
+
function buildMcpServer(config, auth) {
|
|
501
|
+
const server = new McpServer(config.serverInfo, config.instructions ? { instructions: config.instructions } : undefined);
|
|
502
|
+
const services = typeof config.services === "function" ? config.services(auth) : config.services;
|
|
503
|
+
const context = config.context?.(auth);
|
|
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
|
+
});
|
|
514
|
+
config.nativeTools?.(server);
|
|
515
|
+
return server;
|
|
516
|
+
}
|
|
517
|
+
// src/tools/mcp-handler.ts
|
|
518
|
+
import { randomUUID } from "node:crypto";
|
|
519
|
+
import {
|
|
520
|
+
WebStandardStreamableHTTPServerTransport
|
|
521
|
+
} from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
522
|
+
var EVENT_TTL_MS = 10 * 60 * 1000;
|
|
523
|
+
var SESSION_TTL_MS = 30 * 60 * 1000;
|
|
524
|
+
|
|
525
|
+
class InMemoryEventStore {
|
|
526
|
+
events = new Map;
|
|
527
|
+
counter = 0;
|
|
528
|
+
async storeEvent(streamId, message) {
|
|
529
|
+
const eventId = String(++this.counter);
|
|
530
|
+
this.events.set(eventId, { streamId, message, timestamp: Date.now() });
|
|
531
|
+
return eventId;
|
|
532
|
+
}
|
|
533
|
+
async getStreamIdForEventId(eventId) {
|
|
534
|
+
return this.events.get(eventId)?.streamId;
|
|
535
|
+
}
|
|
536
|
+
async replayEventsAfter(lastEventId, { send }) {
|
|
537
|
+
const anchor = this.events.get(lastEventId);
|
|
538
|
+
if (!anchor)
|
|
539
|
+
return "";
|
|
540
|
+
const lastIdNum = Number(lastEventId);
|
|
541
|
+
for (const [eventId, event] of this.events) {
|
|
542
|
+
if (event.streamId === anchor.streamId && Number(eventId) > lastIdNum) {
|
|
543
|
+
await send(eventId, event.message);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return anchor.streamId;
|
|
547
|
+
}
|
|
548
|
+
cleanup() {
|
|
549
|
+
const cutoff = Date.now() - EVENT_TTL_MS;
|
|
550
|
+
for (const [eventId, event] of this.events) {
|
|
551
|
+
if (event.timestamp < cutoff)
|
|
552
|
+
this.events.delete(eventId);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function jsonRpcError(code, message, status) {
|
|
557
|
+
return Response.json({ jsonrpc: "2.0", error: { code, message }, id: null }, { status });
|
|
558
|
+
}
|
|
559
|
+
function createMcpHandler(config) {
|
|
560
|
+
if (Array.isArray(config.services)) {
|
|
561
|
+
validateMcpSchemas(config.services, config.onIncompatibleSchema, config.logger);
|
|
562
|
+
}
|
|
563
|
+
const eventStore = new InMemoryEventStore;
|
|
564
|
+
const sessions = new Map;
|
|
565
|
+
setInterval(() => {
|
|
566
|
+
eventStore.cleanup();
|
|
567
|
+
const cutoff = Date.now() - SESSION_TTL_MS;
|
|
568
|
+
for (const [id, session] of sessions) {
|
|
569
|
+
if (session.lastSeen < cutoff) {
|
|
570
|
+
sessions.delete(id);
|
|
571
|
+
session.transport.close().catch(() => {
|
|
572
|
+
return;
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}, EVENT_TTL_MS).unref();
|
|
577
|
+
return async (req) => {
|
|
578
|
+
const auth = await config.auth(req);
|
|
579
|
+
if (!auth) {
|
|
580
|
+
return jsonRpcError(-32001, "Authorization required", 401);
|
|
581
|
+
}
|
|
582
|
+
const sessionId = req.headers.get("mcp-session-id");
|
|
583
|
+
if (sessionId) {
|
|
584
|
+
const existing = sessions.get(sessionId);
|
|
585
|
+
if (!existing) {
|
|
586
|
+
return jsonRpcError(-32001, "Session not found", 404);
|
|
587
|
+
}
|
|
588
|
+
existing.lastSeen = Date.now();
|
|
589
|
+
if (req.method === "GET") {
|
|
590
|
+
existing.transport.closeStandaloneSSEStream();
|
|
591
|
+
}
|
|
592
|
+
return existing.transport.handleRequest(req);
|
|
593
|
+
}
|
|
594
|
+
const newSessionId = randomUUID();
|
|
595
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
596
|
+
sessionIdGenerator: () => newSessionId,
|
|
597
|
+
eventStore
|
|
598
|
+
});
|
|
599
|
+
const server = buildMcpServer(config, auth);
|
|
600
|
+
sessions.set(newSessionId, { transport, server, lastSeen: Date.now() });
|
|
601
|
+
transport.onclose = () => {
|
|
602
|
+
sessions.delete(newSessionId);
|
|
603
|
+
};
|
|
604
|
+
await server.connect(transport);
|
|
605
|
+
return transport.handleRequest(req);
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
// src/tools/mcp-stdio.ts
|
|
609
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
610
|
+
async function createStdioMcpServer(config) {
|
|
611
|
+
const auth = await config.auth;
|
|
612
|
+
const server = buildMcpServer(config, auth);
|
|
613
|
+
await server.connect(new StdioServerTransport);
|
|
614
|
+
return server;
|
|
615
|
+
}
|
|
616
|
+
// src/tools/remote.ts
|
|
617
|
+
function toArgs(ctx) {
|
|
618
|
+
const { params, input } = ctx;
|
|
619
|
+
return {
|
|
620
|
+
...isRecord(params) ? params : {},
|
|
621
|
+
...isRecord(input) ? input : {}
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function implementRemote(contract, http, options) {
|
|
625
|
+
const client = createClient(contract, http);
|
|
626
|
+
const groupScope = contract.meta.scope ?? "public";
|
|
627
|
+
const methods = {};
|
|
628
|
+
for (const [key, endpoint] of Object.entries(contract.endpoints)) {
|
|
629
|
+
methods[key] = {
|
|
630
|
+
method: endpoint.method,
|
|
631
|
+
path: endpoint.path,
|
|
632
|
+
desc: endpoint.desc,
|
|
633
|
+
toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
|
|
634
|
+
expose: endpoint.expose,
|
|
635
|
+
scope: endpoint.scope ?? groupScope,
|
|
636
|
+
paramsSchema: endpoint.params,
|
|
637
|
+
inputSchema: endpoint.input,
|
|
638
|
+
outputSchema: endpoint.output,
|
|
639
|
+
multipart: endpoint.multipart,
|
|
640
|
+
handler: async (ctx) => {
|
|
641
|
+
const call = client[key];
|
|
642
|
+
if (!call) {
|
|
643
|
+
throw new Error(`implementRemote: endpoint "${key}" is not exposed over HTTP`);
|
|
644
|
+
}
|
|
645
|
+
const args = toArgs(ctx);
|
|
646
|
+
return call(options?.transformArgs ? await options.transformArgs(key, args) : args);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
name: contract.meta.prefix,
|
|
652
|
+
prefix: contract.meta.prefix,
|
|
653
|
+
scope: groupScope,
|
|
654
|
+
methods
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
// src/tools/view-file.ts
|
|
658
|
+
import { lookup } from "node:dns/promises";
|
|
659
|
+
import { readFile, stat } from "node:fs/promises";
|
|
660
|
+
import { isIP } from "node:net";
|
|
661
|
+
import { extname, resolve, sep } from "node:path";
|
|
662
|
+
import { z as z7 } from "zod";
|
|
663
|
+
var MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
664
|
+
var EXT_MIME = {
|
|
665
|
+
".png": "image/png",
|
|
666
|
+
".jpg": "image/jpeg",
|
|
667
|
+
".jpeg": "image/jpeg",
|
|
668
|
+
".gif": "image/gif",
|
|
669
|
+
".webp": "image/webp",
|
|
670
|
+
".mp3": "audio/mpeg",
|
|
671
|
+
".wav": "audio/wav",
|
|
672
|
+
".ogg": "audio/ogg",
|
|
673
|
+
".m4a": "audio/mp4",
|
|
674
|
+
".mp4": "video/mp4",
|
|
675
|
+
".webm": "video/webm",
|
|
676
|
+
".mov": "video/quicktime"
|
|
677
|
+
};
|
|
678
|
+
function isPrivateIp(ip) {
|
|
679
|
+
const family = isIP(ip);
|
|
680
|
+
if (family === 4) {
|
|
681
|
+
const [a, b] = ip.split(".").map(Number);
|
|
682
|
+
if (a === undefined || b === undefined)
|
|
683
|
+
return true;
|
|
684
|
+
return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127;
|
|
685
|
+
}
|
|
686
|
+
if (family === 6) {
|
|
687
|
+
const lower = ip.toLowerCase();
|
|
688
|
+
if (lower === "::1" || lower === "::")
|
|
689
|
+
return true;
|
|
690
|
+
if (lower.startsWith("fe8") || lower.startsWith("fe9") || lower.startsWith("fea") || lower.startsWith("feb") || lower.startsWith("fc") || lower.startsWith("fd")) {
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
const mapped = lower.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
694
|
+
return mapped?.[1] ? isPrivateIp(mapped[1]) : false;
|
|
695
|
+
}
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
async function assertPublicUrl(url) {
|
|
699
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
700
|
+
if (isIP(host)) {
|
|
701
|
+
if (isPrivateIp(host))
|
|
702
|
+
throw new Error("refusing to fetch a private address");
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (host === "localhost" || host.endsWith(".local") || host.endsWith(".internal")) {
|
|
706
|
+
throw new Error("refusing to fetch an internal host");
|
|
707
|
+
}
|
|
708
|
+
const records = await lookup(host, { all: true });
|
|
709
|
+
for (const record of records) {
|
|
710
|
+
if (isPrivateIp(record.address)) {
|
|
711
|
+
throw new Error("refusing to fetch a host that resolves to a private address");
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
async function readCapped(res, max) {
|
|
716
|
+
const reader = res.body?.getReader();
|
|
717
|
+
if (!reader)
|
|
718
|
+
return Buffer.alloc(0);
|
|
719
|
+
const chunks = [];
|
|
720
|
+
let total = 0;
|
|
721
|
+
try {
|
|
722
|
+
while (true) {
|
|
723
|
+
const { done, value } = await reader.read();
|
|
724
|
+
if (done)
|
|
725
|
+
break;
|
|
726
|
+
total += value.length;
|
|
727
|
+
if (total > max) {
|
|
728
|
+
await reader.cancel();
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
chunks.push(value);
|
|
732
|
+
}
|
|
733
|
+
} finally {
|
|
734
|
+
reader.releaseLock();
|
|
735
|
+
}
|
|
736
|
+
return Buffer.concat(chunks);
|
|
737
|
+
}
|
|
738
|
+
async function fetchSource(pathOrUrl, options) {
|
|
739
|
+
const extMime = EXT_MIME[extname(pathOrUrl).toLowerCase()];
|
|
740
|
+
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
741
|
+
const url = new URL(pathOrUrl);
|
|
742
|
+
if (!options.allowPrivateHosts)
|
|
743
|
+
await assertPublicUrl(url);
|
|
744
|
+
const res = await fetch(url);
|
|
745
|
+
if (!res.ok)
|
|
746
|
+
throw new Error(`HTTP ${res.status}`);
|
|
747
|
+
const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
|
|
748
|
+
const mimeType2 = headerMime || extMime || "application/octet-stream";
|
|
749
|
+
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
750
|
+
if (declared > MAX_INLINE_BYTES) {
|
|
751
|
+
await res.body?.cancel();
|
|
752
|
+
return { tooLarge: true, mimeType: mimeType2 };
|
|
753
|
+
}
|
|
754
|
+
const buffer = await readCapped(res, MAX_INLINE_BYTES);
|
|
755
|
+
return buffer ? { buffer, mimeType: mimeType2 } : { tooLarge: true, mimeType: mimeType2 };
|
|
756
|
+
}
|
|
757
|
+
if (!options.baseDir) {
|
|
758
|
+
throw new Error("local file paths are disabled — set baseDir to allow them");
|
|
759
|
+
}
|
|
760
|
+
const root = resolve(options.baseDir);
|
|
761
|
+
const target = resolve(root, pathOrUrl);
|
|
762
|
+
if (target !== root && !target.startsWith(root + sep)) {
|
|
763
|
+
throw new Error("path escapes the allowed directory");
|
|
764
|
+
}
|
|
765
|
+
const info = await stat(target).catch(() => null);
|
|
766
|
+
if (!info?.isFile())
|
|
767
|
+
throw new Error("file not found");
|
|
768
|
+
const mimeType = extMime ?? "application/octet-stream";
|
|
769
|
+
if (info.size > MAX_INLINE_BYTES)
|
|
770
|
+
return { tooLarge: true, mimeType };
|
|
771
|
+
return { buffer: await readFile(target), mimeType };
|
|
772
|
+
}
|
|
773
|
+
async function resolveMedia(pathOrUrl, options = {}) {
|
|
774
|
+
const extMime = EXT_MIME[extname(pathOrUrl).toLowerCase()];
|
|
775
|
+
if (extMime?.startsWith("video/")) {
|
|
776
|
+
return [{ type: "text", text: `[video] ${extMime} — ${pathOrUrl}` }];
|
|
777
|
+
}
|
|
778
|
+
const result = await fetchSource(pathOrUrl, options);
|
|
779
|
+
if ("tooLarge" in result) {
|
|
780
|
+
return [{ type: "text", text: `[${result.mimeType}] too large to inline — ${pathOrUrl}` }];
|
|
781
|
+
}
|
|
782
|
+
const { buffer, mimeType } = result;
|
|
783
|
+
const sizeKb = (buffer.length / 1024).toFixed(0);
|
|
784
|
+
if (mimeType.startsWith("video/")) {
|
|
785
|
+
return [{ type: "text", text: `[video] ${mimeType}, ${sizeKb}KB — ${pathOrUrl}` }];
|
|
786
|
+
}
|
|
787
|
+
if (mimeType.startsWith("image/")) {
|
|
788
|
+
return [
|
|
789
|
+
{ type: "image", data: buffer.toString("base64"), mimeType },
|
|
790
|
+
{ type: "text", text: `[image] ${mimeType}, ${sizeKb}KB` }
|
|
791
|
+
];
|
|
792
|
+
}
|
|
793
|
+
if (mimeType.startsWith("audio/")) {
|
|
794
|
+
return [
|
|
795
|
+
{ type: "audio", data: buffer.toString("base64"), mimeType },
|
|
796
|
+
{ type: "text", text: `[audio] ${mimeType}, ${sizeKb}KB` }
|
|
797
|
+
];
|
|
798
|
+
}
|
|
799
|
+
return [{ type: "text", text: `[${mimeType}] ${sizeKb}KB — ${pathOrUrl}` }];
|
|
800
|
+
}
|
|
801
|
+
function mountViewFile(server, options = {}) {
|
|
802
|
+
server.registerTool("view_file", {
|
|
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.",
|
|
804
|
+
inputSchema: {
|
|
805
|
+
paths: z7.union([z7.string(), z7.array(z7.string())]).describe("Media URL(s) or file path(s) to view")
|
|
806
|
+
}
|
|
807
|
+
}, async (args) => {
|
|
808
|
+
const list = Array.isArray(args.paths) ? args.paths : [args.paths];
|
|
809
|
+
const content = [];
|
|
810
|
+
for (const pathOrUrl of list) {
|
|
811
|
+
try {
|
|
812
|
+
content.push(...await resolveMedia(pathOrUrl, options));
|
|
813
|
+
} catch (err) {
|
|
814
|
+
content.push({
|
|
815
|
+
type: "text",
|
|
816
|
+
text: `[${pathOrUrl}] Error: ${err instanceof Error ? err.message : String(err)}`
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return { content };
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
export {
|
|
824
|
+
withJsonCoercion,
|
|
825
|
+
validateMcpSchemas,
|
|
826
|
+
resolveMedia,
|
|
827
|
+
mountViewFile,
|
|
828
|
+
mountMcp,
|
|
829
|
+
mountAgent,
|
|
830
|
+
implementRemote,
|
|
831
|
+
flattenDiscriminatedUnion,
|
|
832
|
+
createStdioMcpServer,
|
|
833
|
+
createMcpHandler,
|
|
834
|
+
collectTools,
|
|
835
|
+
buildToolManifest,
|
|
836
|
+
buildMcpServer
|
|
837
|
+
};
|