stitchkit 0.0.1 → 0.1.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 +373 -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 +46 -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-7wfkbvss.js +92 -0
- package/dist/index-809wc1tt.js +18 -0
- package/dist/index-gfzn1n4n.js +29 -0
- package/dist/index-ke4mx4ea.js +38 -0
- package/dist/index-n7bmdwmz.js +68 -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/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 +965 -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/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 +124 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/tools/agent.d.ts +14 -0
- package/dist/tools/agent.d.ts.map +1 -0
- package/dist/tools/execute.d.ts +21 -0
- package/dist/tools/execute.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 +42 -0
- package/dist/tools/mcp.d.ts.map +1 -0
- package/dist/tools/mount.d.ts +61 -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 +4 -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 +8 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +654 -0
- package/package.json +136 -4
package/dist/tools.js
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createClient
|
|
3
|
+
} from "./index-5sxnvwb1.js";
|
|
4
|
+
import {
|
|
5
|
+
formatZodError,
|
|
6
|
+
normalizeError
|
|
7
|
+
} from "./index-gfzn1n4n.js";
|
|
8
|
+
import"./index-7wfkbvss.js";
|
|
9
|
+
import {
|
|
10
|
+
isRecord
|
|
11
|
+
} from "./index-809wc1tt.js";
|
|
12
|
+
|
|
13
|
+
// src/tools/agent.ts
|
|
14
|
+
import { tool, zodSchema } from "ai";
|
|
15
|
+
|
|
16
|
+
// src/tools/mount.ts
|
|
17
|
+
import { z as z2 } from "zod";
|
|
18
|
+
|
|
19
|
+
// src/tools/execute.ts
|
|
20
|
+
async function executeToolMethod(method, toolName, rawArgs, context, hooks) {
|
|
21
|
+
const startedAt = Date.now();
|
|
22
|
+
const finish = async (result) => {
|
|
23
|
+
await hooks?.afterToolCall?.(toolName, rawArgs, result, Date.now() - startedAt, context);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
if (hooks?.beforeToolCall) {
|
|
27
|
+
await hooks.beforeToolCall(toolName, rawArgs, context);
|
|
28
|
+
}
|
|
29
|
+
let params;
|
|
30
|
+
if (method.paramsSchema) {
|
|
31
|
+
const result = method.paramsSchema.safeParse(rawArgs);
|
|
32
|
+
if (!result.success) {
|
|
33
|
+
return finish({
|
|
34
|
+
ok: false,
|
|
35
|
+
code: "VALIDATION_ERROR",
|
|
36
|
+
details: { message: `Invalid params: ${formatZodError(result.error)}` }
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
params = result.data;
|
|
40
|
+
}
|
|
41
|
+
let input;
|
|
42
|
+
if (method.inputSchema) {
|
|
43
|
+
const result = method.inputSchema.safeParse(rawArgs);
|
|
44
|
+
if (!result.success) {
|
|
45
|
+
return finish({
|
|
46
|
+
ok: false,
|
|
47
|
+
code: "VALIDATION_ERROR",
|
|
48
|
+
details: { message: `Invalid input: ${formatZodError(result.error)}` }
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
input = result.data;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const ctx = { params, input, ...context };
|
|
55
|
+
const data = await method.handler(ctx);
|
|
56
|
+
const output = data === undefined || data === null ? { status: "ok" } : data;
|
|
57
|
+
return finish({ ok: true, data: output });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
const appErr = normalizeError(err);
|
|
60
|
+
return finish({
|
|
61
|
+
ok: false,
|
|
62
|
+
code: appErr.code,
|
|
63
|
+
details: appErr.details ?? { message: appErr.message },
|
|
64
|
+
...appErr.hint && { hint: appErr.hint }
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/tools/names.ts
|
|
70
|
+
var SINGULAR_EXCEPTIONS = new Set([
|
|
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
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/tools/schema.ts
|
|
106
|
+
import { z } from "zod";
|
|
107
|
+
function needsJsonCoercion(field) {
|
|
108
|
+
if (field instanceof z.ZodArray || field instanceof z.ZodObject)
|
|
109
|
+
return true;
|
|
110
|
+
if (field instanceof z.ZodOptional || field instanceof z.ZodNullable || field instanceof z.ZodDefault) {
|
|
111
|
+
return needsJsonCoercion(field.unwrap());
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
var jsonCoerce = (val) => {
|
|
116
|
+
if (typeof val === "string") {
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(val);
|
|
119
|
+
} catch {
|
|
120
|
+
return val;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return val;
|
|
124
|
+
};
|
|
125
|
+
function withJsonCoercion(schema) {
|
|
126
|
+
const shape = schema.shape;
|
|
127
|
+
const coerced = {};
|
|
128
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
129
|
+
if (!needsJsonCoercion(field)) {
|
|
130
|
+
coerced[key] = field;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
let inner = field;
|
|
134
|
+
const wrappers = [];
|
|
135
|
+
while (inner instanceof z.ZodOptional || inner instanceof z.ZodNullable) {
|
|
136
|
+
if (inner instanceof z.ZodOptional) {
|
|
137
|
+
wrappers.push("optional");
|
|
138
|
+
inner = inner.unwrap();
|
|
139
|
+
} else {
|
|
140
|
+
wrappers.push("nullable");
|
|
141
|
+
inner = inner.unwrap();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
let result = z.preprocess(jsonCoerce, inner);
|
|
145
|
+
for (const wrapper of wrappers.reverse()) {
|
|
146
|
+
result = wrapper === "optional" ? z.optional(result) : z.nullable(result);
|
|
147
|
+
}
|
|
148
|
+
coerced[key] = result;
|
|
149
|
+
}
|
|
150
|
+
return z.object(coerced);
|
|
151
|
+
}
|
|
152
|
+
function flattenDiscriminatedUnion(union) {
|
|
153
|
+
const discriminator = union.def.discriminator;
|
|
154
|
+
const literalValues = [];
|
|
155
|
+
const fieldToVariants = new Map;
|
|
156
|
+
const fieldSchemas = {};
|
|
157
|
+
for (const opt of union.def.options) {
|
|
158
|
+
if (!(opt instanceof z.ZodObject)) {
|
|
159
|
+
throw new Error(`flattenDiscriminatedUnion: option for discriminator '${discriminator}' is not a ZodObject`);
|
|
160
|
+
}
|
|
161
|
+
const discField = opt.shape[discriminator];
|
|
162
|
+
if (!(discField instanceof z.ZodLiteral)) {
|
|
163
|
+
throw new Error(`flattenDiscriminatedUnion: discriminator field '${discriminator}' must be z.literal()`);
|
|
164
|
+
}
|
|
165
|
+
const [literalValue] = discField.def.values;
|
|
166
|
+
if (typeof literalValue !== "string") {
|
|
167
|
+
throw new Error(`flattenDiscriminatedUnion: discriminator literal must be a string (got ${typeof literalValue})`);
|
|
168
|
+
}
|
|
169
|
+
literalValues.push(literalValue);
|
|
170
|
+
for (const [key, field] of Object.entries(opt.shape)) {
|
|
171
|
+
if (key === discriminator)
|
|
172
|
+
continue;
|
|
173
|
+
const variants = fieldToVariants.get(key) ?? [];
|
|
174
|
+
variants.push(literalValue);
|
|
175
|
+
fieldToVariants.set(key, variants);
|
|
176
|
+
if (!(key in fieldSchemas)) {
|
|
177
|
+
fieldSchemas[key] = field instanceof z.ZodOptional ? field : z.optional(field);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const [firstLiteral, ...restLiterals] = literalValues;
|
|
182
|
+
if (!firstLiteral) {
|
|
183
|
+
throw new Error("flattenDiscriminatedUnion: union has no options");
|
|
184
|
+
}
|
|
185
|
+
const shape = {
|
|
186
|
+
[discriminator]: z.enum([firstLiteral, ...restLiterals])
|
|
187
|
+
};
|
|
188
|
+
for (const [key, field] of Object.entries(fieldSchemas)) {
|
|
189
|
+
const variants = fieldToVariants.get(key) ?? [];
|
|
190
|
+
const hint = `Required if ${discriminator} = ${variants.join(" | ")}`;
|
|
191
|
+
shape[key] = field instanceof z.ZodType ? field.describe(hint) : field;
|
|
192
|
+
}
|
|
193
|
+
return z.object(shape);
|
|
194
|
+
}
|
|
195
|
+
function mergeSchemas(paramsSchema, inputSchema) {
|
|
196
|
+
const paramsShape = paramsSchema instanceof z.ZodObject ? paramsSchema.shape : {};
|
|
197
|
+
const inputShape = inputSchema instanceof z.ZodObject ? inputSchema.shape : inputSchema instanceof z.ZodDiscriminatedUnion ? flattenDiscriminatedUnion(inputSchema).shape : {};
|
|
198
|
+
const paramsKeys = Object.keys(paramsShape);
|
|
199
|
+
const inputKeys = new Set(Object.keys(inputShape));
|
|
200
|
+
const conflicts = paramsKeys.filter((k) => inputKeys.has(k));
|
|
201
|
+
if (conflicts.length > 0) {
|
|
202
|
+
throw new Error(`Schema merge conflict: ${conflicts.join(", ")} appear in both params and input`);
|
|
203
|
+
}
|
|
204
|
+
const merged = { ...paramsShape, ...inputShape };
|
|
205
|
+
if (Object.keys(merged).length === 0)
|
|
206
|
+
return z.object({});
|
|
207
|
+
return withJsonCoercion(z.object(merged));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/tools/mount.ts
|
|
211
|
+
function collectTools(service, transport, extend) {
|
|
212
|
+
const tools = [];
|
|
213
|
+
for (const [methodName, method] of Object.entries(service.methods)) {
|
|
214
|
+
if (method.expose && !method.expose.includes(transport))
|
|
215
|
+
continue;
|
|
216
|
+
if (method.multipart)
|
|
217
|
+
continue;
|
|
218
|
+
const name = method.toolName ?? toToolName(service.name, methodName);
|
|
219
|
+
const baseSchema = mergeSchemas(method.paramsSchema, method.inputSchema);
|
|
220
|
+
const shouldExtend = !!extend && (!extend.filter || extend.filter(service, method));
|
|
221
|
+
const schema = shouldExtend ? z2.object({ ...extend?.schema, ...baseSchema.shape }) : baseSchema;
|
|
222
|
+
tools.push({ method, name, schema, shouldExtend });
|
|
223
|
+
}
|
|
224
|
+
return tools;
|
|
225
|
+
}
|
|
226
|
+
function createToolRunner(config) {
|
|
227
|
+
const extendKeys = config.extend ? new Set(Object.keys(config.extend.schema)) : null;
|
|
228
|
+
return async (tool, rawArgs) => {
|
|
229
|
+
let extraContext = {};
|
|
230
|
+
if (tool.shouldExtend && config.extend) {
|
|
231
|
+
extraContext = await config.extend.resolve(rawArgs);
|
|
232
|
+
}
|
|
233
|
+
const cleanArgs = extendKeys ? Object.fromEntries(Object.entries(rawArgs).filter(([key]) => !extendKeys.has(key))) : rawArgs;
|
|
234
|
+
return executeToolMethod(tool.method, tool.name, cleanArgs, { source: config.source, ...config.context, ...extraContext }, config.hooks);
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function formatToolError(result) {
|
|
238
|
+
const err = { error: result.code };
|
|
239
|
+
if (result.details)
|
|
240
|
+
err.details = result.details;
|
|
241
|
+
if (result.hint)
|
|
242
|
+
err._hint = result.hint;
|
|
243
|
+
return err;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/tools/agent.ts
|
|
247
|
+
function formatAgentResult(result) {
|
|
248
|
+
return result.ok ? result.data : formatToolError(result);
|
|
249
|
+
}
|
|
250
|
+
function mountAgent(service, config = {}) {
|
|
251
|
+
const tools = {};
|
|
252
|
+
const runTool = createToolRunner({
|
|
253
|
+
source: "agent",
|
|
254
|
+
extend: config.extend,
|
|
255
|
+
context: config.context,
|
|
256
|
+
hooks: config.hooks
|
|
257
|
+
});
|
|
258
|
+
for (const mountable of collectTools(service, "AGENT", config.extend)) {
|
|
259
|
+
tools[mountable.name] = tool({
|
|
260
|
+
description: mountable.method.desc,
|
|
261
|
+
inputSchema: zodSchema(mountable.schema),
|
|
262
|
+
execute: async (rawArgs) => {
|
|
263
|
+
try {
|
|
264
|
+
const result = await runTool(mountable, rawArgs);
|
|
265
|
+
return formatAgentResult(result);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
const appErr = normalizeError(err);
|
|
268
|
+
return { error: appErr.code, details: appErr.details };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return tools;
|
|
274
|
+
}
|
|
275
|
+
// src/tools/mcp.ts
|
|
276
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
277
|
+
import { z as z3 } from "zod";
|
|
278
|
+
function textBlock(text) {
|
|
279
|
+
return [{ type: "text", text }];
|
|
280
|
+
}
|
|
281
|
+
function formatMcpResult(result, hasOutputSchema) {
|
|
282
|
+
if (result.ok) {
|
|
283
|
+
const content = textBlock(JSON.stringify(result.data, null, 2));
|
|
284
|
+
if (hasOutputSchema && isRecord(result.data)) {
|
|
285
|
+
return { content, structuredContent: result.data };
|
|
286
|
+
}
|
|
287
|
+
return { content };
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
content: textBlock(JSON.stringify(formatToolError(result), null, 2)),
|
|
291
|
+
isError: true
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function mountMcp(mcpServer, services, config = {}) {
|
|
295
|
+
const serviceList = Array.isArray(services) ? services : [services];
|
|
296
|
+
const runTool = createToolRunner({
|
|
297
|
+
source: "mcp",
|
|
298
|
+
extend: config.extend,
|
|
299
|
+
context: config.context,
|
|
300
|
+
hooks: config.hooks
|
|
301
|
+
});
|
|
302
|
+
for (const service of serviceList) {
|
|
303
|
+
for (const mountable of collectTools(service, "MCP", config.extend)) {
|
|
304
|
+
try {
|
|
305
|
+
z3.toJSONSchema(mountable.schema);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.error(`[stitchkit] SKIPPING MCP tool "${mountable.name}" — schema not JSON Schema-compatible: ${err instanceof Error ? err.message : err}`);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
let outputSchema;
|
|
311
|
+
if (mountable.method.outputSchema instanceof z3.ZodObject) {
|
|
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;
|
|
322
|
+
mcpServer.registerTool(mountable.name, toolConfig, async (rawArgs) => {
|
|
323
|
+
try {
|
|
324
|
+
const result = await runTool(mountable, rawArgs);
|
|
325
|
+
return formatMcpResult(result, outputSchema !== undefined);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
const appErr = normalizeError(err);
|
|
328
|
+
return formatMcpResult({ ok: false, code: appErr.code, details: appErr.details }, false);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function buildMcpServer(config, auth) {
|
|
335
|
+
const server = new McpServer(config.serverInfo, config.instructions ? { instructions: config.instructions } : undefined);
|
|
336
|
+
const services = typeof config.services === "function" ? config.services(auth) : config.services;
|
|
337
|
+
const context = config.context?.(auth);
|
|
338
|
+
mountMcp(server, services, { context, hooks: config.hooks });
|
|
339
|
+
config.nativeTools?.(server);
|
|
340
|
+
return server;
|
|
341
|
+
}
|
|
342
|
+
// src/tools/mcp-handler.ts
|
|
343
|
+
import { randomUUID } from "node:crypto";
|
|
344
|
+
import {
|
|
345
|
+
WebStandardStreamableHTTPServerTransport
|
|
346
|
+
} from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
347
|
+
var EVENT_TTL_MS = 10 * 60 * 1000;
|
|
348
|
+
var SESSION_TTL_MS = 30 * 60 * 1000;
|
|
349
|
+
|
|
350
|
+
class InMemoryEventStore {
|
|
351
|
+
events = new Map;
|
|
352
|
+
counter = 0;
|
|
353
|
+
async storeEvent(streamId, message) {
|
|
354
|
+
const eventId = String(++this.counter);
|
|
355
|
+
this.events.set(eventId, { streamId, message, timestamp: Date.now() });
|
|
356
|
+
return eventId;
|
|
357
|
+
}
|
|
358
|
+
async getStreamIdForEventId(eventId) {
|
|
359
|
+
return this.events.get(eventId)?.streamId;
|
|
360
|
+
}
|
|
361
|
+
async replayEventsAfter(lastEventId, { send }) {
|
|
362
|
+
const anchor = this.events.get(lastEventId);
|
|
363
|
+
if (!anchor)
|
|
364
|
+
return "";
|
|
365
|
+
const lastIdNum = Number(lastEventId);
|
|
366
|
+
for (const [eventId, event] of this.events) {
|
|
367
|
+
if (event.streamId === anchor.streamId && Number(eventId) > lastIdNum) {
|
|
368
|
+
await send(eventId, event.message);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return anchor.streamId;
|
|
372
|
+
}
|
|
373
|
+
cleanup() {
|
|
374
|
+
const cutoff = Date.now() - EVENT_TTL_MS;
|
|
375
|
+
for (const [eventId, event] of this.events) {
|
|
376
|
+
if (event.timestamp < cutoff)
|
|
377
|
+
this.events.delete(eventId);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function jsonRpcError(code, message, status) {
|
|
382
|
+
return Response.json({ jsonrpc: "2.0", error: { code, message }, id: null }, { status });
|
|
383
|
+
}
|
|
384
|
+
function createMcpHandler(config) {
|
|
385
|
+
const eventStore = new InMemoryEventStore;
|
|
386
|
+
const sessions = new Map;
|
|
387
|
+
setInterval(() => {
|
|
388
|
+
eventStore.cleanup();
|
|
389
|
+
const cutoff = Date.now() - SESSION_TTL_MS;
|
|
390
|
+
for (const [id, session] of sessions) {
|
|
391
|
+
if (session.lastSeen < cutoff) {
|
|
392
|
+
sessions.delete(id);
|
|
393
|
+
session.transport.close().catch(() => {
|
|
394
|
+
return;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}, EVENT_TTL_MS).unref();
|
|
399
|
+
return async (req) => {
|
|
400
|
+
const auth = await config.auth(req);
|
|
401
|
+
if (!auth) {
|
|
402
|
+
return jsonRpcError(-32001, "Authorization required", 401);
|
|
403
|
+
}
|
|
404
|
+
const sessionId = req.headers.get("mcp-session-id");
|
|
405
|
+
if (sessionId) {
|
|
406
|
+
const existing = sessions.get(sessionId);
|
|
407
|
+
if (!existing) {
|
|
408
|
+
return jsonRpcError(-32001, "Session not found", 404);
|
|
409
|
+
}
|
|
410
|
+
existing.lastSeen = Date.now();
|
|
411
|
+
if (req.method === "GET") {
|
|
412
|
+
existing.transport.closeStandaloneSSEStream();
|
|
413
|
+
}
|
|
414
|
+
return existing.transport.handleRequest(req);
|
|
415
|
+
}
|
|
416
|
+
const newSessionId = randomUUID();
|
|
417
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
418
|
+
sessionIdGenerator: () => newSessionId,
|
|
419
|
+
eventStore
|
|
420
|
+
});
|
|
421
|
+
const server = buildMcpServer(config, auth);
|
|
422
|
+
sessions.set(newSessionId, { transport, server, lastSeen: Date.now() });
|
|
423
|
+
transport.onclose = () => {
|
|
424
|
+
sessions.delete(newSessionId);
|
|
425
|
+
};
|
|
426
|
+
await server.connect(transport);
|
|
427
|
+
return transport.handleRequest(req);
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
// src/tools/mcp-stdio.ts
|
|
431
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
432
|
+
async function createStdioMcpServer(config) {
|
|
433
|
+
const auth = await config.auth;
|
|
434
|
+
const server = buildMcpServer(config, auth);
|
|
435
|
+
await server.connect(new StdioServerTransport);
|
|
436
|
+
return server;
|
|
437
|
+
}
|
|
438
|
+
// src/tools/remote.ts
|
|
439
|
+
function toArgs(ctx) {
|
|
440
|
+
const { params, input } = ctx;
|
|
441
|
+
return {
|
|
442
|
+
...isRecord(params) ? params : {},
|
|
443
|
+
...isRecord(input) ? input : {}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function implementRemote(contract, http, options) {
|
|
447
|
+
const client = createClient(contract, http);
|
|
448
|
+
const groupScope = contract.meta.scope ?? "public";
|
|
449
|
+
const methods = {};
|
|
450
|
+
for (const [key, endpoint] of Object.entries(contract.endpoints)) {
|
|
451
|
+
methods[key] = {
|
|
452
|
+
method: endpoint.method,
|
|
453
|
+
path: endpoint.path,
|
|
454
|
+
desc: endpoint.desc,
|
|
455
|
+
toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
|
|
456
|
+
expose: endpoint.expose,
|
|
457
|
+
scope: endpoint.scope ?? groupScope,
|
|
458
|
+
paramsSchema: endpoint.params,
|
|
459
|
+
inputSchema: endpoint.input,
|
|
460
|
+
outputSchema: endpoint.output,
|
|
461
|
+
multipart: endpoint.multipart,
|
|
462
|
+
handler: async (ctx) => {
|
|
463
|
+
const call = client[key];
|
|
464
|
+
if (!call) {
|
|
465
|
+
throw new Error(`implementRemote: endpoint "${key}" is not exposed over HTTP`);
|
|
466
|
+
}
|
|
467
|
+
const args = toArgs(ctx);
|
|
468
|
+
return call(options?.transformArgs ? await options.transformArgs(key, args) : args);
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
name: contract.meta.prefix,
|
|
474
|
+
prefix: contract.meta.prefix,
|
|
475
|
+
scope: groupScope,
|
|
476
|
+
methods
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
// src/tools/view-file.ts
|
|
480
|
+
import { lookup } from "node:dns/promises";
|
|
481
|
+
import { readFile, stat } from "node:fs/promises";
|
|
482
|
+
import { isIP } from "node:net";
|
|
483
|
+
import { extname, resolve, sep } from "node:path";
|
|
484
|
+
import { z as z4 } from "zod";
|
|
485
|
+
var MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
486
|
+
var EXT_MIME = {
|
|
487
|
+
".png": "image/png",
|
|
488
|
+
".jpg": "image/jpeg",
|
|
489
|
+
".jpeg": "image/jpeg",
|
|
490
|
+
".gif": "image/gif",
|
|
491
|
+
".webp": "image/webp",
|
|
492
|
+
".mp3": "audio/mpeg",
|
|
493
|
+
".wav": "audio/wav",
|
|
494
|
+
".ogg": "audio/ogg",
|
|
495
|
+
".m4a": "audio/mp4",
|
|
496
|
+
".mp4": "video/mp4",
|
|
497
|
+
".webm": "video/webm",
|
|
498
|
+
".mov": "video/quicktime"
|
|
499
|
+
};
|
|
500
|
+
function isPrivateIp(ip) {
|
|
501
|
+
const family = isIP(ip);
|
|
502
|
+
if (family === 4) {
|
|
503
|
+
const [a, b] = ip.split(".").map(Number);
|
|
504
|
+
if (a === undefined || b === undefined)
|
|
505
|
+
return true;
|
|
506
|
+
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;
|
|
507
|
+
}
|
|
508
|
+
if (family === 6) {
|
|
509
|
+
const lower = ip.toLowerCase();
|
|
510
|
+
if (lower === "::1" || lower === "::")
|
|
511
|
+
return true;
|
|
512
|
+
if (lower.startsWith("fe8") || lower.startsWith("fe9") || lower.startsWith("fea") || lower.startsWith("feb") || lower.startsWith("fc") || lower.startsWith("fd")) {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
const mapped = lower.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
516
|
+
return mapped?.[1] ? isPrivateIp(mapped[1]) : false;
|
|
517
|
+
}
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
async function assertPublicUrl(url) {
|
|
521
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
522
|
+
if (isIP(host)) {
|
|
523
|
+
if (isPrivateIp(host))
|
|
524
|
+
throw new Error("refusing to fetch a private address");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (host === "localhost" || host.endsWith(".local") || host.endsWith(".internal")) {
|
|
528
|
+
throw new Error("refusing to fetch an internal host");
|
|
529
|
+
}
|
|
530
|
+
const records = await lookup(host, { all: true });
|
|
531
|
+
for (const record of records) {
|
|
532
|
+
if (isPrivateIp(record.address)) {
|
|
533
|
+
throw new Error("refusing to fetch a host that resolves to a private address");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async function readCapped(res, max) {
|
|
538
|
+
const reader = res.body?.getReader();
|
|
539
|
+
if (!reader)
|
|
540
|
+
return Buffer.alloc(0);
|
|
541
|
+
const chunks = [];
|
|
542
|
+
let total = 0;
|
|
543
|
+
try {
|
|
544
|
+
while (true) {
|
|
545
|
+
const { done, value } = await reader.read();
|
|
546
|
+
if (done)
|
|
547
|
+
break;
|
|
548
|
+
total += value.length;
|
|
549
|
+
if (total > max) {
|
|
550
|
+
await reader.cancel();
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
chunks.push(value);
|
|
554
|
+
}
|
|
555
|
+
} finally {
|
|
556
|
+
reader.releaseLock();
|
|
557
|
+
}
|
|
558
|
+
return Buffer.concat(chunks);
|
|
559
|
+
}
|
|
560
|
+
async function fetchSource(pathOrUrl, options) {
|
|
561
|
+
const extMime = EXT_MIME[extname(pathOrUrl).toLowerCase()];
|
|
562
|
+
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
563
|
+
const url = new URL(pathOrUrl);
|
|
564
|
+
if (!options.allowPrivateHosts)
|
|
565
|
+
await assertPublicUrl(url);
|
|
566
|
+
const res = await fetch(url);
|
|
567
|
+
if (!res.ok)
|
|
568
|
+
throw new Error(`HTTP ${res.status}`);
|
|
569
|
+
const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
|
|
570
|
+
const mimeType2 = headerMime || extMime || "application/octet-stream";
|
|
571
|
+
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
572
|
+
if (declared > MAX_INLINE_BYTES) {
|
|
573
|
+
await res.body?.cancel();
|
|
574
|
+
return { tooLarge: true, mimeType: mimeType2 };
|
|
575
|
+
}
|
|
576
|
+
const buffer = await readCapped(res, MAX_INLINE_BYTES);
|
|
577
|
+
return buffer ? { buffer, mimeType: mimeType2 } : { tooLarge: true, mimeType: mimeType2 };
|
|
578
|
+
}
|
|
579
|
+
if (!options.baseDir) {
|
|
580
|
+
throw new Error("local file paths are disabled — set baseDir to allow them");
|
|
581
|
+
}
|
|
582
|
+
const root = resolve(options.baseDir);
|
|
583
|
+
const target = resolve(root, pathOrUrl);
|
|
584
|
+
if (target !== root && !target.startsWith(root + sep)) {
|
|
585
|
+
throw new Error("path escapes the allowed directory");
|
|
586
|
+
}
|
|
587
|
+
const info = await stat(target).catch(() => null);
|
|
588
|
+
if (!info?.isFile())
|
|
589
|
+
throw new Error("file not found");
|
|
590
|
+
const mimeType = extMime ?? "application/octet-stream";
|
|
591
|
+
if (info.size > MAX_INLINE_BYTES)
|
|
592
|
+
return { tooLarge: true, mimeType };
|
|
593
|
+
return { buffer: await readFile(target), mimeType };
|
|
594
|
+
}
|
|
595
|
+
async function resolveMedia(pathOrUrl, options = {}) {
|
|
596
|
+
const extMime = EXT_MIME[extname(pathOrUrl).toLowerCase()];
|
|
597
|
+
if (extMime?.startsWith("video/")) {
|
|
598
|
+
return [{ type: "text", text: `[video] ${extMime} — ${pathOrUrl}` }];
|
|
599
|
+
}
|
|
600
|
+
const result = await fetchSource(pathOrUrl, options);
|
|
601
|
+
if ("tooLarge" in result) {
|
|
602
|
+
return [{ type: "text", text: `[${result.mimeType}] too large to inline — ${pathOrUrl}` }];
|
|
603
|
+
}
|
|
604
|
+
const { buffer, mimeType } = result;
|
|
605
|
+
const sizeKb = (buffer.length / 1024).toFixed(0);
|
|
606
|
+
if (mimeType.startsWith("video/")) {
|
|
607
|
+
return [{ type: "text", text: `[video] ${mimeType}, ${sizeKb}KB — ${pathOrUrl}` }];
|
|
608
|
+
}
|
|
609
|
+
if (mimeType.startsWith("image/")) {
|
|
610
|
+
return [
|
|
611
|
+
{ type: "image", data: buffer.toString("base64"), mimeType },
|
|
612
|
+
{ type: "text", text: `[image] ${mimeType}, ${sizeKb}KB` }
|
|
613
|
+
];
|
|
614
|
+
}
|
|
615
|
+
if (mimeType.startsWith("audio/")) {
|
|
616
|
+
return [
|
|
617
|
+
{ type: "audio", data: buffer.toString("base64"), mimeType },
|
|
618
|
+
{ type: "text", text: `[audio] ${mimeType}, ${sizeKb}KB` }
|
|
619
|
+
];
|
|
620
|
+
}
|
|
621
|
+
return [{ type: "text", text: `[${mimeType}] ${sizeKb}KB — ${pathOrUrl}` }];
|
|
622
|
+
}
|
|
623
|
+
function mountViewFile(server, options = {}) {
|
|
624
|
+
server.registerTool("view_file", {
|
|
625
|
+
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
|
+
inputSchema: {
|
|
627
|
+
paths: z4.union([z4.string(), z4.array(z4.string())]).describe("Media URL(s) or file path(s) to view")
|
|
628
|
+
}
|
|
629
|
+
}, async (args) => {
|
|
630
|
+
const list = Array.isArray(args.paths) ? args.paths : [args.paths];
|
|
631
|
+
const content = [];
|
|
632
|
+
for (const pathOrUrl of list) {
|
|
633
|
+
try {
|
|
634
|
+
content.push(...await resolveMedia(pathOrUrl, options));
|
|
635
|
+
} catch (err) {
|
|
636
|
+
content.push({
|
|
637
|
+
type: "text",
|
|
638
|
+
text: `[${pathOrUrl}] Error: ${err instanceof Error ? err.message : String(err)}`
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return { content };
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
export {
|
|
646
|
+
resolveMedia,
|
|
647
|
+
mountViewFile,
|
|
648
|
+
mountMcp,
|
|
649
|
+
mountAgent,
|
|
650
|
+
implementRemote,
|
|
651
|
+
createStdioMcpServer,
|
|
652
|
+
createMcpHandler,
|
|
653
|
+
buildMcpServer
|
|
654
|
+
};
|