stitchkit 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/http.d.ts +5 -0
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/{index-a35v22fh.js → index-0t460v7k.js} +12 -4
- package/dist/{index-n7bmdwmz.js → index-4b1j82gp.js} +8 -4
- package/dist/{index-5sxnvwb1.js → index-4kz3aqpx.js} +2 -2
- package/dist/{index-v2z2v3mq.js → index-dpj1jcys.js} +150 -60
- package/dist/index-kzfs85xp.js +9 -0
- package/dist/index-mwmpw6j1.js +60 -0
- package/dist/index-q6ja2qwq.js +7 -0
- package/dist/index.js +4 -3
- package/dist/internal/errors.d.ts +13 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/safe-json.d.ts +19 -0
- package/dist/internal/safe-json.d.ts.map +1 -0
- package/dist/internal/within-dir.d.ts +7 -0
- package/dist/internal/within-dir.d.ts.map +1 -0
- package/dist/node.js +6 -3
- package/dist/observability/context.d.ts +9 -1
- package/dist/observability/context.d.ts.map +1 -1
- package/dist/observability/index.js +35 -15
- package/dist/observability/sanitize.d.ts.map +1 -1
- package/dist/server/context.d.ts +3 -2
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/create.d.ts.map +1 -1
- package/dist/server/event-bus.d.ts +10 -1
- package/dist/server/event-bus.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 +86 -32
- package/dist/server/logger.d.ts +5 -4
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/middleware/auth.d.ts +24 -1
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/server/middleware/cookies.d.ts.map +1 -1
- package/dist/server/middleware/cors.d.ts +7 -0
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/multipart.d.ts +2 -1
- package/dist/server/multipart.d.ts.map +1 -1
- package/dist/server/rate-limit.d.ts +7 -2
- package/dist/server/rate-limit.d.ts.map +1 -1
- package/dist/server/request.d.ts +27 -4
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/router.d.ts +4 -2
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/stream.d.ts +3 -1
- package/dist/server/stream.d.ts.map +1 -1
- package/dist/server/types.d.ts +12 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/tools/coerce.d.ts +8 -4
- package/dist/tools/coerce.d.ts.map +1 -1
- package/dist/tools/execute.d.ts +1 -1
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/manifest.d.ts +4 -0
- package/dist/tools/manifest.d.ts.map +1 -1
- package/dist/tools/mcp-handler.d.ts.map +1 -1
- package/dist/tools/mount.d.ts +2 -2
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +140 -93
- package/package.json +1 -1
- package/dist/index-ke4mx4ea.js +0 -38
package/dist/tools.js
CHANGED
|
@@ -1,41 +1,77 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createClient
|
|
3
|
-
} from "./index-
|
|
3
|
+
} from "./index-4kz3aqpx.js";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
} from "./index-
|
|
5
|
+
isWithinDir
|
|
6
|
+
} from "./index-q6ja2qwq.js";
|
|
7
7
|
import {
|
|
8
8
|
formatZodError,
|
|
9
|
-
normalizeError
|
|
10
|
-
|
|
9
|
+
normalizeError,
|
|
10
|
+
validateHandlerOutput
|
|
11
|
+
} from "./index-0t460v7k.js";
|
|
11
12
|
import"./index-kckky6zw.js";
|
|
13
|
+
import {
|
|
14
|
+
isUnsafeKey,
|
|
15
|
+
safeJsonParse
|
|
16
|
+
} from "./index-kzfs85xp.js";
|
|
17
|
+
import {
|
|
18
|
+
isRecord
|
|
19
|
+
} from "./index-809wc1tt.js";
|
|
12
20
|
|
|
13
21
|
// src/tools/agent.ts
|
|
14
22
|
import { tool, zodSchema } from "ai";
|
|
15
23
|
|
|
16
|
-
// src/tools/
|
|
24
|
+
// src/tools/coerce.ts
|
|
17
25
|
import { z } from "zod";
|
|
26
|
+
function needsJsonCoercion(field) {
|
|
27
|
+
if (field instanceof z.ZodArray || field instanceof z.ZodObject)
|
|
28
|
+
return true;
|
|
29
|
+
if (field instanceof z.ZodOptional || field instanceof z.ZodNullable || field instanceof z.ZodDefault) {
|
|
30
|
+
return needsJsonCoercion(field.unwrap());
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
function coerceJsonArgs(args, schema) {
|
|
35
|
+
if (!(schema instanceof z.ZodObject))
|
|
36
|
+
return args;
|
|
37
|
+
const shape = schema.shape;
|
|
38
|
+
const out = {};
|
|
39
|
+
for (const [key, value] of Object.entries(args)) {
|
|
40
|
+
const field = shape[key];
|
|
41
|
+
if (field && needsJsonCoercion(field) && typeof value === "string") {
|
|
42
|
+
try {
|
|
43
|
+
out[key] = safeJsonParse(value);
|
|
44
|
+
continue;
|
|
45
|
+
} catch {}
|
|
46
|
+
}
|
|
47
|
+
out[key] = value;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/tools/schema.ts
|
|
53
|
+
import { z as z2 } from "zod";
|
|
18
54
|
function objectShapeKeys(schema) {
|
|
19
|
-
return schema instanceof
|
|
55
|
+
return schema instanceof z2.ZodObject ? Object.keys(schema.shape) : [];
|
|
20
56
|
}
|
|
21
57
|
function mergeSchemas(paramsSchema, inputSchema) {
|
|
22
|
-
if (paramsSchema && !(paramsSchema instanceof
|
|
58
|
+
if (paramsSchema && !(paramsSchema instanceof z2.ZodObject)) {
|
|
23
59
|
throw new Error("Tool params schema must be a z.object()");
|
|
24
60
|
}
|
|
25
|
-
const paramsObject = paramsSchema instanceof
|
|
61
|
+
const paramsObject = paramsSchema instanceof z2.ZodObject ? paramsSchema : undefined;
|
|
26
62
|
if (!inputSchema) {
|
|
27
|
-
return paramsObject ??
|
|
63
|
+
return paramsObject ?? z2.object({});
|
|
28
64
|
}
|
|
29
|
-
if (inputSchema instanceof
|
|
65
|
+
if (inputSchema instanceof z2.ZodObject) {
|
|
30
66
|
if (paramsObject) {
|
|
31
67
|
const conflicts = Object.keys(paramsObject.shape).filter((key) => (key in inputSchema.shape));
|
|
32
68
|
if (conflicts.length > 0) {
|
|
33
69
|
throw new Error(`Schema merge conflict: ${conflicts.join(", ")} appear in both params and input`);
|
|
34
70
|
}
|
|
35
71
|
}
|
|
36
|
-
return
|
|
72
|
+
return z2.object({ ...paramsObject?.shape ?? {}, ...inputSchema.shape });
|
|
37
73
|
}
|
|
38
|
-
return paramsObject ?
|
|
74
|
+
return paramsObject ? z2.intersection(paramsObject, inputSchema) : inputSchema;
|
|
39
75
|
}
|
|
40
76
|
|
|
41
77
|
// src/tools/execute.ts
|
|
@@ -48,24 +84,34 @@ function toolResultFromError(err) {
|
|
|
48
84
|
...appErr.hint && { hint: appErr.hint }
|
|
49
85
|
};
|
|
50
86
|
}
|
|
51
|
-
async function executeToolMethod(method, toolName, rawArgs, context, hooks, lifecycle) {
|
|
87
|
+
async function executeToolMethod(method, toolName, rawArgs, context, hooks, lifecycle, coerceJson = false) {
|
|
52
88
|
const startedAt = Date.now();
|
|
53
89
|
const finish = async (result) => {
|
|
54
90
|
await hooks?.afterToolCall?.(toolName, rawArgs, result, Date.now() - startedAt, context);
|
|
55
91
|
return result;
|
|
56
92
|
};
|
|
57
93
|
if (hooks?.beforeToolCall) {
|
|
58
|
-
|
|
94
|
+
try {
|
|
95
|
+
await hooks.beforeToolCall(toolName, rawArgs, context);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return finish(toolResultFromError(err));
|
|
98
|
+
}
|
|
59
99
|
}
|
|
60
100
|
const paramKeys = new Set(objectShapeKeys(method.paramsSchema));
|
|
61
|
-
|
|
62
|
-
|
|
101
|
+
let paramArgs = {};
|
|
102
|
+
let inputArgs = {};
|
|
63
103
|
for (const [key, value] of Object.entries(rawArgs)) {
|
|
104
|
+
if (isUnsafeKey(key))
|
|
105
|
+
continue;
|
|
64
106
|
if (paramKeys.has(key))
|
|
65
107
|
paramArgs[key] = value;
|
|
66
108
|
else
|
|
67
109
|
inputArgs[key] = value;
|
|
68
110
|
}
|
|
111
|
+
if (coerceJson) {
|
|
112
|
+
paramArgs = coerceJsonArgs(paramArgs, method.paramsSchema);
|
|
113
|
+
inputArgs = coerceJsonArgs(inputArgs, method.inputSchema);
|
|
114
|
+
}
|
|
69
115
|
let params;
|
|
70
116
|
if (method.paramsSchema) {
|
|
71
117
|
const result = method.paramsSchema.safeParse(paramArgs);
|
|
@@ -102,19 +148,17 @@ async function executeToolMethod(method, toolName, rawArgs, context, hooks, life
|
|
|
102
148
|
data = transformed;
|
|
103
149
|
}
|
|
104
150
|
if (method.outputSchema) {
|
|
105
|
-
const
|
|
106
|
-
if (!
|
|
151
|
+
const checked = validateHandlerOutput(method.outputSchema, data);
|
|
152
|
+
if (!checked.ok) {
|
|
107
153
|
return finish({
|
|
108
154
|
ok: false,
|
|
109
155
|
code: "INTERNAL_SERVER_ERROR",
|
|
110
|
-
details: {
|
|
111
|
-
message: `Handler output does not match the contract: ${formatZodError(parsed.error)}`
|
|
112
|
-
}
|
|
156
|
+
details: { message: checked.message }
|
|
113
157
|
});
|
|
114
158
|
}
|
|
115
|
-
data =
|
|
159
|
+
data = checked.data;
|
|
116
160
|
}
|
|
117
|
-
const output = data === undefined || data === null ? { status: "ok" } : data;
|
|
161
|
+
const output = (data === undefined || data === null) && !method.outputSchema ? { status: "ok" } : data;
|
|
118
162
|
return finish({ ok: true, data: output });
|
|
119
163
|
} catch (err) {
|
|
120
164
|
return finish(toolResultFromError(err));
|
|
@@ -124,50 +168,6 @@ async function executeToolMethod(method, toolName, rawArgs, context, hooks, life
|
|
|
124
168
|
// src/tools/mount.ts
|
|
125
169
|
import { z as z4 } from "zod";
|
|
126
170
|
|
|
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
171
|
// src/tools/flatten.ts
|
|
172
172
|
import { z as z3 } from "zod";
|
|
173
173
|
function flattenDiscriminatedUnion(union) {
|
|
@@ -209,7 +209,7 @@ function flattenDiscriminatedUnion(union) {
|
|
|
209
209
|
for (const [key, field] of Object.entries(fieldSchemas)) {
|
|
210
210
|
const variants = fieldToVariants.get(key) ?? [];
|
|
211
211
|
const hint = `Required if ${discriminator} = ${variants.join(" | ")}`;
|
|
212
|
-
shape[key] = field
|
|
212
|
+
shape[key] = field.describe(hint);
|
|
213
213
|
}
|
|
214
214
|
return z3.object(shape);
|
|
215
215
|
}
|
|
@@ -262,7 +262,7 @@ function applyExtend(base, extra) {
|
|
|
262
262
|
return z4.intersection(z4.object(extra), base);
|
|
263
263
|
}
|
|
264
264
|
function collectTools(service, transport, config = {}) {
|
|
265
|
-
const { extend,
|
|
265
|
+
const { extend, flattenUnionInput = false } = config;
|
|
266
266
|
const tools = [];
|
|
267
267
|
for (const [methodName, method] of Object.entries(service.methods)) {
|
|
268
268
|
if (method.expose && !method.expose.includes(transport))
|
|
@@ -274,9 +274,6 @@ function collectTools(service, transport, config = {}) {
|
|
|
274
274
|
if (flattenUnionInput && baseSchema instanceof z4.ZodDiscriminatedUnion) {
|
|
275
275
|
baseSchema = flattenDiscriminatedUnion(baseSchema);
|
|
276
276
|
}
|
|
277
|
-
if (coerceJsonArgs && baseSchema instanceof z4.ZodObject) {
|
|
278
|
-
baseSchema = withJsonCoercion(baseSchema);
|
|
279
|
-
}
|
|
280
277
|
const shouldExtend = !!extend && (!extend.filter || extend.filter(service, method));
|
|
281
278
|
const schema = shouldExtend && extend ? applyExtend(baseSchema, extend.schema) : baseSchema;
|
|
282
279
|
tools.push({ method, name, schema, shouldExtend });
|
|
@@ -291,7 +288,7 @@ function createToolRunner(config) {
|
|
|
291
288
|
extraContext = await config.extend.resolve(rawArgs);
|
|
292
289
|
}
|
|
293
290
|
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);
|
|
291
|
+
return executeToolMethod(tool.method, tool.name, cleanArgs, { ...config.context, ...extraContext, source: config.source }, config.hooks, config.lifecycle, config.coerceJsonArgs ?? true);
|
|
295
292
|
};
|
|
296
293
|
}
|
|
297
294
|
function formatToolError(result, toolName, errorHint) {
|
|
@@ -321,12 +318,12 @@ function mountAgent(services, config = {}) {
|
|
|
321
318
|
context: config.context,
|
|
322
319
|
hooks: config.hooks,
|
|
323
320
|
lifecycle: config.lifecycle,
|
|
324
|
-
errorHint: config.errorHint
|
|
321
|
+
errorHint: config.errorHint,
|
|
322
|
+
coerceJsonArgs: config.coerceJsonArgs
|
|
325
323
|
});
|
|
326
324
|
for (const service of serviceList) {
|
|
327
325
|
for (const mountable of collectTools(service, "AGENT", {
|
|
328
326
|
extend: config.extend,
|
|
329
|
-
coerceJsonArgs: config.coerceJsonArgs,
|
|
330
327
|
flattenUnionInput: config.flattenUnionInput
|
|
331
328
|
})) {
|
|
332
329
|
if (mountable.name in tools) {
|
|
@@ -364,11 +361,15 @@ function toJsonSchema(schema, io) {
|
|
|
364
361
|
|
|
365
362
|
// src/tools/manifest.ts
|
|
366
363
|
function buildToolManifest(tools) {
|
|
367
|
-
return tools.map((t) =>
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
364
|
+
return tools.map((t) => {
|
|
365
|
+
let inputSchema;
|
|
366
|
+
try {
|
|
367
|
+
inputSchema = toJsonSchema(t.schema, "input");
|
|
368
|
+
} catch {
|
|
369
|
+
inputSchema = {};
|
|
370
|
+
}
|
|
371
|
+
return { name: t.name, description: t.method.desc, inputSchema };
|
|
372
|
+
});
|
|
372
373
|
}
|
|
373
374
|
// src/tools/mcp.ts
|
|
374
375
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -468,14 +469,14 @@ function mountMcp(mcpServer, services, config = {}) {
|
|
|
468
469
|
context: config.context,
|
|
469
470
|
hooks: config.hooks,
|
|
470
471
|
lifecycle: config.lifecycle,
|
|
471
|
-
errorHint: config.errorHint
|
|
472
|
+
errorHint: config.errorHint,
|
|
473
|
+
coerceJsonArgs: config.coerceJsonArgs
|
|
472
474
|
});
|
|
473
475
|
const seen = new Set;
|
|
474
476
|
const failures = [];
|
|
475
477
|
for (const service of serviceList) {
|
|
476
478
|
for (const mountable of collectTools(service, "MCP", {
|
|
477
479
|
extend: config.extend,
|
|
478
|
-
coerceJsonArgs: config.coerceJsonArgs,
|
|
479
480
|
flattenUnionInput: config.flattenUnionInput
|
|
480
481
|
})) {
|
|
481
482
|
const prepared = prepareMcpTool(mountable, policy, config.logger, failures, seen);
|
|
@@ -521,6 +522,9 @@ import {
|
|
|
521
522
|
} from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
522
523
|
var EVENT_TTL_MS = 10 * 60 * 1000;
|
|
523
524
|
var SESSION_TTL_MS = 30 * 60 * 1000;
|
|
525
|
+
var SWEEP_INTERVAL_MS = 60 * 1000;
|
|
526
|
+
var MAX_EVENTS = 1e4;
|
|
527
|
+
var MAX_SESSIONS = 1000;
|
|
524
528
|
|
|
525
529
|
class InMemoryEventStore {
|
|
526
530
|
events = new Map;
|
|
@@ -528,6 +532,11 @@ class InMemoryEventStore {
|
|
|
528
532
|
async storeEvent(streamId, message) {
|
|
529
533
|
const eventId = String(++this.counter);
|
|
530
534
|
this.events.set(eventId, { streamId, message, timestamp: Date.now() });
|
|
535
|
+
if (this.events.size > MAX_EVENTS) {
|
|
536
|
+
const oldest = this.events.keys().next().value;
|
|
537
|
+
if (oldest !== undefined)
|
|
538
|
+
this.events.delete(oldest);
|
|
539
|
+
}
|
|
531
540
|
return eventId;
|
|
532
541
|
}
|
|
533
542
|
async getStreamIdForEventId(eventId) {
|
|
@@ -562,18 +571,21 @@ function createMcpHandler(config) {
|
|
|
562
571
|
}
|
|
563
572
|
const eventStore = new InMemoryEventStore;
|
|
564
573
|
const sessions = new Map;
|
|
574
|
+
const closeTransport = (transport) => {
|
|
575
|
+
transport.close().catch((err) => {
|
|
576
|
+
console.error("[stitchkit] MCP transport close failed:", err);
|
|
577
|
+
});
|
|
578
|
+
};
|
|
565
579
|
setInterval(() => {
|
|
566
580
|
eventStore.cleanup();
|
|
567
581
|
const cutoff = Date.now() - SESSION_TTL_MS;
|
|
568
582
|
for (const [id, session] of sessions) {
|
|
569
583
|
if (session.lastSeen < cutoff) {
|
|
570
584
|
sessions.delete(id);
|
|
571
|
-
session.transport
|
|
572
|
-
return;
|
|
573
|
-
});
|
|
585
|
+
closeTransport(session.transport);
|
|
574
586
|
}
|
|
575
587
|
}
|
|
576
|
-
},
|
|
588
|
+
}, SWEEP_INTERVAL_MS).unref();
|
|
577
589
|
return async (req) => {
|
|
578
590
|
const auth = await config.auth(req);
|
|
579
591
|
if (!auth) {
|
|
@@ -591,6 +603,22 @@ function createMcpHandler(config) {
|
|
|
591
603
|
}
|
|
592
604
|
return existing.transport.handleRequest(req);
|
|
593
605
|
}
|
|
606
|
+
if (sessions.size >= MAX_SESSIONS) {
|
|
607
|
+
let oldestId;
|
|
608
|
+
let oldestSeen = Number.POSITIVE_INFINITY;
|
|
609
|
+
for (const [id, session] of sessions) {
|
|
610
|
+
if (session.lastSeen < oldestSeen) {
|
|
611
|
+
oldestSeen = session.lastSeen;
|
|
612
|
+
oldestId = id;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (oldestId !== undefined) {
|
|
616
|
+
const evicted = sessions.get(oldestId);
|
|
617
|
+
sessions.delete(oldestId);
|
|
618
|
+
if (evicted)
|
|
619
|
+
closeTransport(evicted.transport);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
594
622
|
const newSessionId = randomUUID();
|
|
595
623
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
596
624
|
sessionIdGenerator: () => newSessionId,
|
|
@@ -658,8 +686,10 @@ function implementRemote(contract, http, options) {
|
|
|
658
686
|
import { lookup } from "node:dns/promises";
|
|
659
687
|
import { readFile, stat } from "node:fs/promises";
|
|
660
688
|
import { isIP } from "node:net";
|
|
661
|
-
import { extname, resolve
|
|
689
|
+
import { extname, resolve } from "node:path";
|
|
662
690
|
import { z as z7 } from "zod";
|
|
691
|
+
var NUMERIC_HOST = /^(0x[0-9a-f]+|[0-9.]+)$/i;
|
|
692
|
+
var MAX_REDIRECTS = 5;
|
|
663
693
|
var MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
664
694
|
var EXT_MIME = {
|
|
665
695
|
".png": "image/png",
|
|
@@ -702,6 +732,9 @@ async function assertPublicUrl(url) {
|
|
|
702
732
|
throw new Error("refusing to fetch a private address");
|
|
703
733
|
return;
|
|
704
734
|
}
|
|
735
|
+
if (NUMERIC_HOST.test(host)) {
|
|
736
|
+
throw new Error("refusing to fetch a non-canonical numeric host");
|
|
737
|
+
}
|
|
705
738
|
if (host === "localhost" || host.endsWith(".local") || host.endsWith(".internal")) {
|
|
706
739
|
throw new Error("refusing to fetch an internal host");
|
|
707
740
|
}
|
|
@@ -712,6 +745,22 @@ async function assertPublicUrl(url) {
|
|
|
712
745
|
}
|
|
713
746
|
}
|
|
714
747
|
}
|
|
748
|
+
async function fetchGuarded(start, allowPrivate) {
|
|
749
|
+
let url = start;
|
|
750
|
+
for (let hop = 0;hop <= MAX_REDIRECTS; hop++) {
|
|
751
|
+
if (!allowPrivate)
|
|
752
|
+
await assertPublicUrl(url);
|
|
753
|
+
const res = await fetch(url, { redirect: "manual" });
|
|
754
|
+
if (res.status < 300 || res.status >= 400)
|
|
755
|
+
return res;
|
|
756
|
+
const location = res.headers.get("location");
|
|
757
|
+
if (!location)
|
|
758
|
+
return res;
|
|
759
|
+
await res.body?.cancel();
|
|
760
|
+
url = new URL(location, url);
|
|
761
|
+
}
|
|
762
|
+
throw new Error("too many redirects");
|
|
763
|
+
}
|
|
715
764
|
async function readCapped(res, max) {
|
|
716
765
|
const reader = res.body?.getReader();
|
|
717
766
|
if (!reader)
|
|
@@ -739,9 +788,7 @@ async function fetchSource(pathOrUrl, options) {
|
|
|
739
788
|
const extMime = EXT_MIME[extname(pathOrUrl).toLowerCase()];
|
|
740
789
|
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
741
790
|
const url = new URL(pathOrUrl);
|
|
742
|
-
|
|
743
|
-
await assertPublicUrl(url);
|
|
744
|
-
const res = await fetch(url);
|
|
791
|
+
const res = await fetchGuarded(url, options.allowPrivateHosts ?? false);
|
|
745
792
|
if (!res.ok)
|
|
746
793
|
throw new Error(`HTTP ${res.status}`);
|
|
747
794
|
const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
|
|
@@ -759,7 +806,7 @@ async function fetchSource(pathOrUrl, options) {
|
|
|
759
806
|
}
|
|
760
807
|
const root = resolve(options.baseDir);
|
|
761
808
|
const target = resolve(root, pathOrUrl);
|
|
762
|
-
if (
|
|
809
|
+
if (!isWithinDir(root, target)) {
|
|
763
810
|
throw new Error("path escapes the allowed directory");
|
|
764
811
|
}
|
|
765
812
|
const info = await stat(target).catch(() => null);
|
|
@@ -821,7 +868,6 @@ function mountViewFile(server, options = {}) {
|
|
|
821
868
|
});
|
|
822
869
|
}
|
|
823
870
|
export {
|
|
824
|
-
withJsonCoercion,
|
|
825
871
|
validateMcpSchemas,
|
|
826
872
|
resolveMedia,
|
|
827
873
|
mountViewFile,
|
|
@@ -832,6 +878,7 @@ export {
|
|
|
832
878
|
createStdioMcpServer,
|
|
833
879
|
createMcpHandler,
|
|
834
880
|
collectTools,
|
|
881
|
+
coerceJsonArgs,
|
|
835
882
|
buildToolManifest,
|
|
836
883
|
buildMcpServer
|
|
837
884
|
};
|
package/package.json
CHANGED
package/dist/index-ke4mx4ea.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// src/server/request.ts
|
|
2
|
-
function generateTraceId() {
|
|
3
|
-
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
4
|
-
}
|
|
5
|
-
function resolveTraceId(req) {
|
|
6
|
-
const header = req.headers.get("x-request-id") ?? req.headers.get("x-trace-id");
|
|
7
|
-
const trimmed = header?.trim();
|
|
8
|
-
if (trimmed && trimmed.length <= 128 && /^[\w.-]+$/.test(trimmed)) {
|
|
9
|
-
return trimmed;
|
|
10
|
-
}
|
|
11
|
-
return generateTraceId();
|
|
12
|
-
}
|
|
13
|
-
function extractIp(req) {
|
|
14
|
-
const forwarded = req.headers.get("x-forwarded-for");
|
|
15
|
-
if (forwarded)
|
|
16
|
-
return (forwarded.split(",")[0] ?? "").trim().replace("::ffff:", "");
|
|
17
|
-
const realIp = req.headers.get("x-real-ip");
|
|
18
|
-
if (realIp)
|
|
19
|
-
return realIp.trim().replace("::ffff:", "");
|
|
20
|
-
return "";
|
|
21
|
-
}
|
|
22
|
-
function getClientInfo(req) {
|
|
23
|
-
return {
|
|
24
|
-
ipAddress: extractIp(req) || undefined,
|
|
25
|
-
userAgent: req.headers.get("user-agent") ?? undefined
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
function parseQueryParams(url) {
|
|
29
|
-
const query = {};
|
|
30
|
-
for (const key of new Set(url.searchParams.keys())) {
|
|
31
|
-
const values = url.searchParams.getAll(key);
|
|
32
|
-
const [first] = values;
|
|
33
|
-
query[key] = values.length === 1 && first !== undefined ? first : values;
|
|
34
|
-
}
|
|
35
|
-
return query;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export { generateTraceId, resolveTraceId, extractIp, getClientInfo, parseQueryParams };
|