stitchkit 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -10
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/http.d.ts +5 -0
- package/dist/browser/http.d.ts.map +1 -1
- package/dist/browser/socket-io.d.ts +17 -0
- package/dist/browser/socket-io.d.ts.map +1 -1
- package/dist/cli.d.ts +23 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +21 -0
- package/dist/contract/define.d.ts +35 -2
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +2 -1
- package/dist/index-0ed3bx43.js +33 -0
- package/dist/index-0ma1eqv4.js +1007 -0
- package/dist/index-1cf8jkhf.js +219 -0
- package/dist/index-37x76zdn.js +4 -0
- package/dist/{index-v2z2v3mq.js → index-3qmtdfpa.js} +304 -62
- package/dist/index-48ffdxgk.js +6 -0
- package/dist/{index-5sxnvwb1.js → index-78q1qm7v.js} +6 -3
- package/dist/{index-kckky6zw.js → index-afzt3nmx.js} +1 -1
- package/dist/{index-n7bmdwmz.js → index-d4rwrjbc.js} +8 -4
- package/dist/index-kzfs85xp.js +9 -0
- package/dist/index-mwmpw6j1.js +60 -0
- package/dist/{index-a35v22fh.js → index-vhdvv00d.js} +13 -5
- package/dist/index-x3fcszf8.js +8 -0
- package/dist/index.js +17 -4
- package/dist/internal/errors.d.ts +13 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/http-input.d.ts +13 -0
- package/dist/internal/http-input.d.ts.map +1 -0
- package/dist/internal/safe-json.d.ts +19 -0
- package/dist/internal/safe-json.d.ts.map +1 -0
- package/dist/internal/secure-fetch.d.ts +27 -0
- package/dist/internal/secure-fetch.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.d.ts +10 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +41 -7
- package/dist/observability/audit.d.ts.map +1 -1
- package/dist/observability/context.d.ts +9 -1
- package/dist/observability/context.d.ts.map +1 -1
- package/dist/observability/event.d.ts +4 -0
- package/dist/observability/event.d.ts.map +1 -1
- package/dist/observability/index.js +38 -15
- package/dist/observability/sanitize.d.ts.map +1 -1
- package/dist/react.js +2 -0
- 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/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +6 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +203 -193
- package/dist/server/logger.d.ts +5 -4
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/middleware/auth.d.ts +41 -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/middleware/pkce.d.ts +17 -0
- package/dist/server/middleware/pkce.d.ts.map +1 -0
- package/dist/server/multipart.d.ts +2 -1
- package/dist/server/multipart.d.ts.map +1 -1
- package/dist/server/node.d.ts +9 -0
- package/dist/server/node.d.ts.map +1 -1
- package/dist/server/openapi.d.ts +52 -0
- package/dist/server/openapi.d.ts.map +1 -0
- 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/socket-io.d.ts +60 -15
- package/dist/server/socket-io.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 +17 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/websocket.d.ts +73 -0
- package/dist/server/websocket.d.ts.map +1 -0
- package/dist/tools/cli-args.d.ts +68 -0
- package/dist/tools/cli-args.d.ts.map +1 -0
- package/dist/tools/cli-format.d.ts +39 -0
- package/dist/tools/cli-format.d.ts.map +1 -0
- package/dist/tools/cli-wait.d.ts +45 -0
- package/dist/tools/cli-wait.d.ts.map +1 -0
- package/dist/tools/cli.d.ts +77 -0
- package/dist/tools/cli.d.ts.map +1 -0
- 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/json-schema.d.ts +29 -4
- package/dist/tools/json-schema.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-app.d.ts +59 -0
- package/dist/tools/mcp-app.d.ts.map +1 -0
- package/dist/tools/mcp-handler.d.ts +18 -0
- package/dist/tools/mcp-handler.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts +9 -0
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mount-download.d.ts +30 -0
- package/dist/tools/mount-download.d.ts.map +1 -0
- package/dist/tools/mount-upload.d.ts +19 -0
- package/dist/tools/mount-upload.d.ts.map +1 -0
- package/dist/tools/mount-wait.d.ts +36 -0
- package/dist/tools/mount-wait.d.ts.map +1 -0
- package/dist/tools/mount.d.ts +8 -4
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/native-result.d.ts +17 -0
- package/dist/tools/native-result.d.ts.map +1 -0
- package/dist/tools/oauth-metadata.d.ts +28 -0
- package/dist/tools/oauth-metadata.d.ts.map +1 -0
- package/dist/tools/oauth-provider.d.ts +85 -0
- package/dist/tools/oauth-provider.d.ts.map +1 -0
- package/dist/tools/remote.d.ts +2 -2
- package/dist/tools/remote.d.ts.map +1 -1
- package/dist/tools/toolkit.d.ts +51 -0
- package/dist/tools/toolkit.d.ts.map +1 -0
- package/dist/tools/view-file.d.ts +12 -0
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools/wait-core.d.ts +33 -0
- package/dist/tools/wait-core.d.ts.map +1 -0
- package/dist/tools.d.ts +13 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +637 -424
- package/package.json +22 -9
- package/dist/index-ke4mx4ea.js +0 -38
package/dist/tools.js
CHANGED
|
@@ -1,317 +1,42 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ApiError,
|
|
2
3
|
createClient
|
|
3
|
-
} from "./index-
|
|
4
|
+
} from "./index-78q1qm7v.js";
|
|
5
|
+
import {
|
|
6
|
+
coerceJsonArgs,
|
|
7
|
+
collectTools,
|
|
8
|
+
createCli,
|
|
9
|
+
createToolRunner,
|
|
10
|
+
fetchGuarded,
|
|
11
|
+
flattenDiscriminatedUnion,
|
|
12
|
+
formatToolError,
|
|
13
|
+
pollUntil,
|
|
14
|
+
readCapped,
|
|
15
|
+
toolResultFromError
|
|
16
|
+
} from "./index-0ma1eqv4.js";
|
|
17
|
+
import {
|
|
18
|
+
signJwt,
|
|
19
|
+
verifyPkce
|
|
20
|
+
} from "./index-1cf8jkhf.js";
|
|
21
|
+
import"./index-48ffdxgk.js";
|
|
22
|
+
import {
|
|
23
|
+
toJsonSchema
|
|
24
|
+
} from "./index-0ed3bx43.js";
|
|
25
|
+
import {
|
|
26
|
+
isWithinDir
|
|
27
|
+
} from "./index-x3fcszf8.js";
|
|
28
|
+
import"./index-vhdvv00d.js";
|
|
29
|
+
import {
|
|
30
|
+
AppError
|
|
31
|
+
} from "./index-afzt3nmx.js";
|
|
32
|
+
import"./index-kzfs85xp.js";
|
|
4
33
|
import {
|
|
5
34
|
isRecord
|
|
6
35
|
} from "./index-809wc1tt.js";
|
|
7
|
-
import
|
|
8
|
-
formatZodError,
|
|
9
|
-
normalizeError
|
|
10
|
-
} from "./index-a35v22fh.js";
|
|
11
|
-
import"./index-kckky6zw.js";
|
|
36
|
+
import"./index-37x76zdn.js";
|
|
12
37
|
|
|
13
38
|
// src/tools/agent.ts
|
|
14
39
|
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
40
|
function mountAgent(services, config = {}) {
|
|
316
41
|
const serviceList = Array.isArray(services) ? services : [services];
|
|
317
42
|
const tools = {};
|
|
@@ -321,12 +46,12 @@ function mountAgent(services, config = {}) {
|
|
|
321
46
|
context: config.context,
|
|
322
47
|
hooks: config.hooks,
|
|
323
48
|
lifecycle: config.lifecycle,
|
|
324
|
-
errorHint: config.errorHint
|
|
49
|
+
errorHint: config.errorHint,
|
|
50
|
+
coerceJsonArgs: config.coerceJsonArgs
|
|
325
51
|
});
|
|
326
52
|
for (const service of serviceList) {
|
|
327
53
|
for (const mountable of collectTools(service, "AGENT", {
|
|
328
54
|
extend: config.extend,
|
|
329
|
-
coerceJsonArgs: config.coerceJsonArgs,
|
|
330
55
|
flattenUnionInput: config.flattenUnionInput
|
|
331
56
|
})) {
|
|
332
57
|
if (mountable.name in tools) {
|
|
@@ -351,28 +76,45 @@ function mountAgent(services, config = {}) {
|
|
|
351
76
|
}
|
|
352
77
|
return tools;
|
|
353
78
|
}
|
|
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
79
|
// src/tools/manifest.ts
|
|
366
80
|
function buildToolManifest(tools) {
|
|
367
|
-
return tools.map((t) =>
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
81
|
+
return tools.map((t) => {
|
|
82
|
+
let inputSchema;
|
|
83
|
+
try {
|
|
84
|
+
inputSchema = toJsonSchema(t.schema, "input");
|
|
85
|
+
} catch {
|
|
86
|
+
inputSchema = {};
|
|
87
|
+
}
|
|
88
|
+
return { name: t.name, description: t.method.desc, inputSchema };
|
|
89
|
+
});
|
|
372
90
|
}
|
|
373
91
|
// src/tools/mcp.ts
|
|
374
92
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
375
|
-
import { z
|
|
93
|
+
import { z } from "zod";
|
|
94
|
+
|
|
95
|
+
// src/tools/mcp-app.ts
|
|
96
|
+
import { readFileSync } from "node:fs";
|
|
97
|
+
import { createRequire } from "node:module";
|
|
98
|
+
var RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
99
|
+
var EXT_APPS_BUNDLE_PLACEHOLDER = "/*__EXT_APPS_BUNDLE__*/";
|
|
100
|
+
var require2 = createRequire(import.meta.url);
|
|
101
|
+
function inlineMcpAppBundle(html) {
|
|
102
|
+
if (!html.includes(EXT_APPS_BUNDLE_PLACEHOLDER))
|
|
103
|
+
return html;
|
|
104
|
+
let bundlePath;
|
|
105
|
+
try {
|
|
106
|
+
bundlePath = require2.resolve("@modelcontextprotocol/ext-apps/app-with-deps");
|
|
107
|
+
} catch {
|
|
108
|
+
throw new Error("[stitchkit] inlineMcpAppBundle: '@modelcontextprotocol/ext-apps' is not installed. " + "Add it as a dependency to serve MCP App widgets.");
|
|
109
|
+
}
|
|
110
|
+
const bundle = readFileSync(bundlePath, "utf8").replace(/export\{([^}]+)\};?\s*$/, (_match, body) => `globalThis.ExtApps={${body.split(",").map((pair) => {
|
|
111
|
+
const [local, exported] = pair.split(" as ").map((s) => s.trim());
|
|
112
|
+
return `${exported ?? local}:${local}`;
|
|
113
|
+
}).join(",")}};`);
|
|
114
|
+
return html.replace(EXT_APPS_BUNDLE_PLACEHOLDER, () => bundle);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/tools/mcp.ts
|
|
376
118
|
function textBlock(text) {
|
|
377
119
|
return [{ type: "text", text }];
|
|
378
120
|
}
|
|
@@ -403,10 +145,10 @@ function probeSchema(schema, io) {
|
|
|
403
145
|
function resolveOutputSchema(outputSchema) {
|
|
404
146
|
if (!outputSchema)
|
|
405
147
|
return null;
|
|
406
|
-
if (outputSchema instanceof
|
|
148
|
+
if (outputSchema instanceof z.ZodObject) {
|
|
407
149
|
return { schema: outputSchema, mode: "direct" };
|
|
408
150
|
}
|
|
409
|
-
return { schema:
|
|
151
|
+
return { schema: z.object({ result: outputSchema }), mode: "wrapped" };
|
|
410
152
|
}
|
|
411
153
|
function reportIncompatible(message, policy, logger, failures) {
|
|
412
154
|
if (policy === "throw") {
|
|
@@ -430,7 +172,7 @@ function prepareMcpTool(mountable, policy, logger, failures, seen) {
|
|
|
430
172
|
throw new Error(`Duplicate MCP tool name "${mountable.name}" across mounted services`);
|
|
431
173
|
}
|
|
432
174
|
seen.add(mountable.name);
|
|
433
|
-
if (!(mountable.schema instanceof
|
|
175
|
+
if (!(mountable.schema instanceof z.ZodObject)) {
|
|
434
176
|
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
177
|
return null;
|
|
436
178
|
}
|
|
@@ -468,14 +210,14 @@ function mountMcp(mcpServer, services, config = {}) {
|
|
|
468
210
|
context: config.context,
|
|
469
211
|
hooks: config.hooks,
|
|
470
212
|
lifecycle: config.lifecycle,
|
|
471
|
-
errorHint: config.errorHint
|
|
213
|
+
errorHint: config.errorHint,
|
|
214
|
+
coerceJsonArgs: config.coerceJsonArgs
|
|
472
215
|
});
|
|
473
216
|
const seen = new Set;
|
|
474
217
|
const failures = [];
|
|
475
218
|
for (const service of serviceList) {
|
|
476
219
|
for (const mountable of collectTools(service, "MCP", {
|
|
477
220
|
extend: config.extend,
|
|
478
|
-
coerceJsonArgs: config.coerceJsonArgs,
|
|
479
221
|
flattenUnionInput: config.flattenUnionInput
|
|
480
222
|
})) {
|
|
481
223
|
const prepared = prepareMcpTool(mountable, policy, config.logger, failures, seen);
|
|
@@ -484,6 +226,15 @@ function mountMcp(mcpServer, services, config = {}) {
|
|
|
484
226
|
const toolConfig = { description: mountable.method.desc, inputSchema: mountable.schema };
|
|
485
227
|
if (prepared.outputSchema)
|
|
486
228
|
toolConfig.outputSchema = prepared.outputSchema;
|
|
229
|
+
if (mountable.method.annotations) {
|
|
230
|
+
toolConfig.annotations = mountable.method.annotations;
|
|
231
|
+
}
|
|
232
|
+
if (mountable.method.ui) {
|
|
233
|
+
toolConfig._meta = {
|
|
234
|
+
ui: mountable.method.ui,
|
|
235
|
+
"ui/resourceUri": mountable.method.ui.resourceUri
|
|
236
|
+
};
|
|
237
|
+
}
|
|
487
238
|
mcpServer.registerTool(mountable.name, toolConfig, async (rawArgs) => {
|
|
488
239
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
489
240
|
try {
|
|
@@ -512,15 +263,75 @@ function buildMcpServer(config, auth) {
|
|
|
512
263
|
errorHint: config.errorHint
|
|
513
264
|
});
|
|
514
265
|
config.nativeTools?.(server);
|
|
266
|
+
for (const resource of config.resources ?? []) {
|
|
267
|
+
mountMcpResource(server, resource);
|
|
268
|
+
}
|
|
515
269
|
return server;
|
|
516
270
|
}
|
|
271
|
+
function mountMcpResource(server, resource) {
|
|
272
|
+
server.registerResource(resource.name, resource.uri, { mimeType: resource.mimeType ?? RESOURCE_MIME_TYPE }, async () => ({
|
|
273
|
+
contents: [
|
|
274
|
+
{
|
|
275
|
+
uri: resource.uri,
|
|
276
|
+
mimeType: resource.mimeType ?? RESOURCE_MIME_TYPE,
|
|
277
|
+
text: await resource.read(),
|
|
278
|
+
...resource.ui && { _meta: { ui: resource.ui } }
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
517
283
|
// src/tools/mcp-handler.ts
|
|
518
284
|
import { randomUUID } from "node:crypto";
|
|
519
285
|
import {
|
|
520
286
|
WebStandardStreamableHTTPServerTransport
|
|
521
287
|
} from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
288
|
+
|
|
289
|
+
// src/tools/oauth-metadata.ts
|
|
290
|
+
var PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
|
|
291
|
+
function metadataPath(resource) {
|
|
292
|
+
const { pathname } = new URL(resource);
|
|
293
|
+
return pathname === "/" ? PROTECTED_RESOURCE_PATH : `${PROTECTED_RESOURCE_PATH}${pathname}`;
|
|
294
|
+
}
|
|
295
|
+
function protectedResourceMetadataUrl(resource) {
|
|
296
|
+
return `${new URL(resource).origin}${metadataPath(resource)}`;
|
|
297
|
+
}
|
|
298
|
+
function wwwAuthenticateHeader(resource) {
|
|
299
|
+
return `Bearer resource_metadata="${protectedResourceMetadataUrl(resource)}"`;
|
|
300
|
+
}
|
|
301
|
+
var PUBLIC_CORS = {
|
|
302
|
+
"Access-Control-Allow-Origin": "*",
|
|
303
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
304
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type"
|
|
305
|
+
};
|
|
306
|
+
function oauthProtectedResourceRoute(config) {
|
|
307
|
+
const body = JSON.stringify({
|
|
308
|
+
resource: config.resource,
|
|
309
|
+
authorization_servers: config.authorizationServers,
|
|
310
|
+
...config.scopesSupported && { scopes_supported: config.scopesSupported },
|
|
311
|
+
bearer_methods_supported: ["header"]
|
|
312
|
+
});
|
|
313
|
+
return {
|
|
314
|
+
method: "ALL",
|
|
315
|
+
path: metadataPath(config.resource),
|
|
316
|
+
handler: (req) => {
|
|
317
|
+
if (req.method === "OPTIONS")
|
|
318
|
+
return new Response(null, { status: 204, headers: PUBLIC_CORS });
|
|
319
|
+
if (req.method !== "GET")
|
|
320
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
321
|
+
return new Response(body, {
|
|
322
|
+
status: 200,
|
|
323
|
+
headers: { "Content-Type": "application/json", ...PUBLIC_CORS }
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/tools/mcp-handler.ts
|
|
522
330
|
var EVENT_TTL_MS = 10 * 60 * 1000;
|
|
523
331
|
var SESSION_TTL_MS = 30 * 60 * 1000;
|
|
332
|
+
var SWEEP_INTERVAL_MS = 60 * 1000;
|
|
333
|
+
var MAX_EVENTS = 1e4;
|
|
334
|
+
var MAX_SESSIONS = 1000;
|
|
524
335
|
|
|
525
336
|
class InMemoryEventStore {
|
|
526
337
|
events = new Map;
|
|
@@ -528,6 +339,11 @@ class InMemoryEventStore {
|
|
|
528
339
|
async storeEvent(streamId, message) {
|
|
529
340
|
const eventId = String(++this.counter);
|
|
530
341
|
this.events.set(eventId, { streamId, message, timestamp: Date.now() });
|
|
342
|
+
if (this.events.size > MAX_EVENTS) {
|
|
343
|
+
const oldest = this.events.keys().next().value;
|
|
344
|
+
if (oldest !== undefined)
|
|
345
|
+
this.events.delete(oldest);
|
|
346
|
+
}
|
|
531
347
|
return eventId;
|
|
532
348
|
}
|
|
533
349
|
async getStreamIdForEventId(eventId) {
|
|
@@ -553,32 +369,52 @@ class InMemoryEventStore {
|
|
|
553
369
|
}
|
|
554
370
|
}
|
|
555
371
|
}
|
|
556
|
-
function jsonRpcError(code, message, status) {
|
|
557
|
-
return Response.json({ jsonrpc: "2.0", error: { code, message }, id: null }, { status });
|
|
372
|
+
function jsonRpcError(code, message, status, headers) {
|
|
373
|
+
return Response.json({ jsonrpc: "2.0", error: { code, message }, id: null }, { status, headers });
|
|
558
374
|
}
|
|
559
375
|
function createMcpHandler(config) {
|
|
560
376
|
if (Array.isArray(config.services)) {
|
|
561
377
|
validateMcpSchemas(config.services, config.onIncompatibleSchema, config.logger);
|
|
562
378
|
}
|
|
379
|
+
const unauthorized = () => {
|
|
380
|
+
const headers = config.protectedResource ? { "WWW-Authenticate": wwwAuthenticateHeader(config.protectedResource.resource) } : undefined;
|
|
381
|
+
return jsonRpcError(-32001, "Authorization required", 401, headers);
|
|
382
|
+
};
|
|
383
|
+
if (config.stateless) {
|
|
384
|
+
return async (req) => {
|
|
385
|
+
const auth = await config.auth(req);
|
|
386
|
+
if (!auth)
|
|
387
|
+
return unauthorized();
|
|
388
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
389
|
+
sessionIdGenerator: undefined,
|
|
390
|
+
enableJsonResponse: true
|
|
391
|
+
});
|
|
392
|
+
const server = buildMcpServer(config, auth);
|
|
393
|
+
await server.connect(transport);
|
|
394
|
+
return transport.handleRequest(req);
|
|
395
|
+
};
|
|
396
|
+
}
|
|
563
397
|
const eventStore = new InMemoryEventStore;
|
|
564
398
|
const sessions = new Map;
|
|
399
|
+
const closeTransport = (transport) => {
|
|
400
|
+
transport.close().catch((err) => {
|
|
401
|
+
console.error("[stitchkit] MCP transport close failed:", err);
|
|
402
|
+
});
|
|
403
|
+
};
|
|
565
404
|
setInterval(() => {
|
|
566
405
|
eventStore.cleanup();
|
|
567
406
|
const cutoff = Date.now() - SESSION_TTL_MS;
|
|
568
407
|
for (const [id, session] of sessions) {
|
|
569
408
|
if (session.lastSeen < cutoff) {
|
|
570
409
|
sessions.delete(id);
|
|
571
|
-
session.transport
|
|
572
|
-
return;
|
|
573
|
-
});
|
|
410
|
+
closeTransport(session.transport);
|
|
574
411
|
}
|
|
575
412
|
}
|
|
576
|
-
},
|
|
413
|
+
}, SWEEP_INTERVAL_MS).unref();
|
|
577
414
|
return async (req) => {
|
|
578
415
|
const auth = await config.auth(req);
|
|
579
|
-
if (!auth)
|
|
580
|
-
return
|
|
581
|
-
}
|
|
416
|
+
if (!auth)
|
|
417
|
+
return unauthorized();
|
|
582
418
|
const sessionId = req.headers.get("mcp-session-id");
|
|
583
419
|
if (sessionId) {
|
|
584
420
|
const existing = sessions.get(sessionId);
|
|
@@ -591,6 +427,22 @@ function createMcpHandler(config) {
|
|
|
591
427
|
}
|
|
592
428
|
return existing.transport.handleRequest(req);
|
|
593
429
|
}
|
|
430
|
+
if (sessions.size >= MAX_SESSIONS) {
|
|
431
|
+
let oldestId;
|
|
432
|
+
let oldestSeen = Number.POSITIVE_INFINITY;
|
|
433
|
+
for (const [id, session] of sessions) {
|
|
434
|
+
if (session.lastSeen < oldestSeen) {
|
|
435
|
+
oldestSeen = session.lastSeen;
|
|
436
|
+
oldestId = id;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (oldestId !== undefined) {
|
|
440
|
+
const evicted = sessions.get(oldestId);
|
|
441
|
+
sessions.delete(oldestId);
|
|
442
|
+
if (evicted)
|
|
443
|
+
closeTransport(evicted.transport);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
594
446
|
const newSessionId = randomUUID();
|
|
595
447
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
596
448
|
sessionIdGenerator: () => newSessionId,
|
|
@@ -613,6 +465,376 @@ async function createStdioMcpServer(config) {
|
|
|
613
465
|
await server.connect(new StdioServerTransport);
|
|
614
466
|
return server;
|
|
615
467
|
}
|
|
468
|
+
// src/tools/mount-download.ts
|
|
469
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
470
|
+
import { basename, extname, join } from "node:path";
|
|
471
|
+
|
|
472
|
+
// src/tools/native-result.ts
|
|
473
|
+
function textResult(text, isError = false) {
|
|
474
|
+
const content = [{ type: "text", text }];
|
|
475
|
+
return isError ? { content, isError: true } : { content };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/tools/mount-download.ts
|
|
479
|
+
var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
|
|
480
|
+
var MIME_EXT = {
|
|
481
|
+
"image/png": ".png",
|
|
482
|
+
"image/jpeg": ".jpg",
|
|
483
|
+
"image/webp": ".webp",
|
|
484
|
+
"image/gif": ".gif",
|
|
485
|
+
"video/mp4": ".mp4",
|
|
486
|
+
"video/webm": ".webm",
|
|
487
|
+
"audio/mpeg": ".mp3",
|
|
488
|
+
"audio/wav": ".wav",
|
|
489
|
+
"audio/ogg": ".ogg"
|
|
490
|
+
};
|
|
491
|
+
function extensionFor(url, mime) {
|
|
492
|
+
if (MIME_EXT[mime])
|
|
493
|
+
return MIME_EXT[mime];
|
|
494
|
+
try {
|
|
495
|
+
const ext = extname(new URL(url).pathname).toLowerCase();
|
|
496
|
+
if (ext)
|
|
497
|
+
return ext;
|
|
498
|
+
} catch {}
|
|
499
|
+
return ".bin";
|
|
500
|
+
}
|
|
501
|
+
function baseNameFor(url) {
|
|
502
|
+
try {
|
|
503
|
+
const path = new URL(url).pathname;
|
|
504
|
+
return (basename(path, extname(path)) || "download").slice(0, 60);
|
|
505
|
+
} catch {
|
|
506
|
+
return "download";
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function mountDownload(server, config) {
|
|
510
|
+
server.registerTool(config.name ?? "download", { description: config.description, inputSchema: config.inputSchema }, async (rawArgs) => {
|
|
511
|
+
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
512
|
+
try {
|
|
513
|
+
const url = await config.resolveUrl(args);
|
|
514
|
+
if (!url)
|
|
515
|
+
return textResult("Nothing to download.", true);
|
|
516
|
+
const res = await fetchGuarded(new URL(url), config.allowPrivateHosts ?? false);
|
|
517
|
+
if (!res.ok)
|
|
518
|
+
return textResult(`Download failed: HTTP ${res.status}`, true);
|
|
519
|
+
const mimeType = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() || "application/octet-stream";
|
|
520
|
+
const max = config.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
521
|
+
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
522
|
+
if (declared > max) {
|
|
523
|
+
await res.body?.cancel();
|
|
524
|
+
return textResult(`Download failed: file exceeds the ${max}-byte cap`, true);
|
|
525
|
+
}
|
|
526
|
+
const buffer = await readCapped(res, max);
|
|
527
|
+
if (!buffer)
|
|
528
|
+
return textResult(`Download failed: file exceeds the ${max}-byte cap`, true);
|
|
529
|
+
const dir = config.dirFromArgs?.(args) ?? config.defaultDir;
|
|
530
|
+
await mkdir(dir, { recursive: true });
|
|
531
|
+
const filePath = join(dir, `${baseNameFor(url)}${extensionFor(url, mimeType)}`);
|
|
532
|
+
await writeFile(filePath, buffer);
|
|
533
|
+
return textResult(JSON.stringify({ path: filePath, size: buffer.length, mimeType }, null, 2));
|
|
534
|
+
} catch (err) {
|
|
535
|
+
return textResult(`Download failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
// src/tools/mount-upload.ts
|
|
540
|
+
import { z as z2 } from "zod";
|
|
541
|
+
function mountUpload(server, config) {
|
|
542
|
+
server.registerTool(config.name ?? "upload", {
|
|
543
|
+
description: config.description,
|
|
544
|
+
inputSchema: { path: z2.string().describe("Path to a local file on this machine") }
|
|
545
|
+
}, async (rawArgs) => {
|
|
546
|
+
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
547
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
548
|
+
if (!path)
|
|
549
|
+
return textResult("Provide `path`.", true);
|
|
550
|
+
try {
|
|
551
|
+
const uploaded = await config.upload(path);
|
|
552
|
+
return textResult(JSON.stringify(uploaded ?? null, null, 2));
|
|
553
|
+
} catch (err) {
|
|
554
|
+
return textResult(`Upload failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
// src/tools/mount-wait.ts
|
|
559
|
+
function mountWait(server, config) {
|
|
560
|
+
server.registerTool(config.name ?? "wait", { description: config.description, inputSchema: config.inputSchema }, async (rawArgs) => {
|
|
561
|
+
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
562
|
+
try {
|
|
563
|
+
const { state, timedOut } = await pollUntil({
|
|
564
|
+
poll: () => config.poll(args),
|
|
565
|
+
done: config.done,
|
|
566
|
+
backoff: config.backoff,
|
|
567
|
+
timeoutSec: config.timeoutFromArgs?.(args) ?? config.defaultTimeout
|
|
568
|
+
});
|
|
569
|
+
const rendered = config.render ? config.render(state, timedOut) : { text: JSON.stringify(state, null, 2), isError: timedOut };
|
|
570
|
+
return textResult(rendered.text, rendered.isError);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
return textResult(`Wait failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
// src/tools/oauth-provider.ts
|
|
577
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
578
|
+
var PUBLIC_CORS2 = {
|
|
579
|
+
"Access-Control-Allow-Origin": "*",
|
|
580
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
581
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type"
|
|
582
|
+
};
|
|
583
|
+
var AS_METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
584
|
+
var AUTH_CODE_TTL_MS = 60000;
|
|
585
|
+
function json(body, status = 200, extraHeaders) {
|
|
586
|
+
return new Response(JSON.stringify(body), {
|
|
587
|
+
status,
|
|
588
|
+
headers: { "Content-Type": "application/json", ...PUBLIC_CORS2, ...extraHeaders }
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
function oauthError(error, description, status = 400) {
|
|
592
|
+
return json({ error, error_description: description }, status);
|
|
593
|
+
}
|
|
594
|
+
function isHttpUri(value) {
|
|
595
|
+
try {
|
|
596
|
+
const url = new URL(value);
|
|
597
|
+
if (url.protocol === "https:")
|
|
598
|
+
return true;
|
|
599
|
+
if (url.protocol !== "http:")
|
|
600
|
+
return false;
|
|
601
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
602
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
603
|
+
} catch {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function redirectWith(uri, params) {
|
|
608
|
+
const url = new URL(uri);
|
|
609
|
+
for (const [key, value] of Object.entries(params))
|
|
610
|
+
url.searchParams.set(key, value);
|
|
611
|
+
return new Response(null, { status: 302, headers: { Location: url.toString() } });
|
|
612
|
+
}
|
|
613
|
+
async function readForm(req) {
|
|
614
|
+
const contentType = req.headers.get("content-type") ?? "";
|
|
615
|
+
if (contentType.includes("application/json")) {
|
|
616
|
+
const body = await req.json().catch(() => null);
|
|
617
|
+
const params = new URLSearchParams;
|
|
618
|
+
if (isRecord(body)) {
|
|
619
|
+
for (const [key, value] of Object.entries(body)) {
|
|
620
|
+
if (typeof value === "string")
|
|
621
|
+
params.set(key, value);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return params;
|
|
625
|
+
}
|
|
626
|
+
return new URLSearchParams(await req.text());
|
|
627
|
+
}
|
|
628
|
+
function mountOAuthProvider(config) {
|
|
629
|
+
const base = config.basePath ?? "/oauth";
|
|
630
|
+
const ttl = config.accessTokenTtlSec ?? 3600;
|
|
631
|
+
const registerPath = `${base}/register`;
|
|
632
|
+
const authorizePath = `${base}/authorize`;
|
|
633
|
+
const tokenPath = `${base}/token`;
|
|
634
|
+
const metadataRoute = {
|
|
635
|
+
method: "ALL",
|
|
636
|
+
path: AS_METADATA_PATH,
|
|
637
|
+
handler: (req) => {
|
|
638
|
+
if (req.method === "OPTIONS")
|
|
639
|
+
return new Response(null, { status: 204, headers: PUBLIC_CORS2 });
|
|
640
|
+
return json({
|
|
641
|
+
issuer: config.issuer,
|
|
642
|
+
authorization_endpoint: `${config.issuer}${authorizePath}`,
|
|
643
|
+
token_endpoint: `${config.issuer}${tokenPath}`,
|
|
644
|
+
registration_endpoint: `${config.issuer}${registerPath}`,
|
|
645
|
+
response_types_supported: ["code"],
|
|
646
|
+
grant_types_supported: config.refreshTokens ? ["authorization_code", "refresh_token"] : ["authorization_code"],
|
|
647
|
+
code_challenge_methods_supported: ["S256"],
|
|
648
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
649
|
+
...config.scopesSupported && { scopes_supported: config.scopesSupported }
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
const registerRoute = {
|
|
654
|
+
method: "ALL",
|
|
655
|
+
path: registerPath,
|
|
656
|
+
handler: async (req) => {
|
|
657
|
+
if (req.method === "OPTIONS")
|
|
658
|
+
return new Response(null, { status: 204, headers: PUBLIC_CORS2 });
|
|
659
|
+
if (req.method !== "POST")
|
|
660
|
+
return oauthError("invalid_request", "POST required", 405);
|
|
661
|
+
const meta = await req.json().catch(() => null);
|
|
662
|
+
if (!isRecord(meta)) {
|
|
663
|
+
return oauthError("invalid_client_metadata", "Body must be a JSON object");
|
|
664
|
+
}
|
|
665
|
+
const redirectUris = meta.redirect_uris;
|
|
666
|
+
if (!Array.isArray(redirectUris) || redirectUris.length === 0 || !redirectUris.every((u) => typeof u === "string" && isHttpUri(u))) {
|
|
667
|
+
return oauthError("invalid_redirect_uri", "redirect_uris must be a non-empty array of absolute https URLs (http is allowed only on a loopback host)");
|
|
668
|
+
}
|
|
669
|
+
const client = await config.clients.register({
|
|
670
|
+
redirectUris,
|
|
671
|
+
clientName: typeof meta.client_name === "string" ? meta.client_name : undefined,
|
|
672
|
+
tokenEndpointAuthMethod: typeof meta.token_endpoint_auth_method === "string" ? meta.token_endpoint_auth_method : undefined
|
|
673
|
+
});
|
|
674
|
+
return json({
|
|
675
|
+
client_id: client.clientId,
|
|
676
|
+
redirect_uris: client.redirectUris,
|
|
677
|
+
token_endpoint_auth_method: "none",
|
|
678
|
+
grant_types: config.refreshTokens ? ["authorization_code", "refresh_token"] : ["authorization_code"],
|
|
679
|
+
response_types: ["code"],
|
|
680
|
+
...client.clientName && { client_name: client.clientName }
|
|
681
|
+
}, 201);
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
const authorizeRoute = {
|
|
685
|
+
method: "ALL",
|
|
686
|
+
path: authorizePath,
|
|
687
|
+
handler: async (req) => {
|
|
688
|
+
if (req.method === "OPTIONS")
|
|
689
|
+
return new Response(null, { status: 204, headers: PUBLIC_CORS2 });
|
|
690
|
+
const url = new URL(req.url);
|
|
691
|
+
const p = url.searchParams;
|
|
692
|
+
const clientId = p.get("client_id");
|
|
693
|
+
const redirectUri = p.get("redirect_uri");
|
|
694
|
+
const responseType = p.get("response_type");
|
|
695
|
+
const codeChallenge = p.get("code_challenge");
|
|
696
|
+
const codeChallengeMethod = p.get("code_challenge_method") ?? "S256";
|
|
697
|
+
const resource = p.get("resource");
|
|
698
|
+
const scope = p.get("scope") ?? undefined;
|
|
699
|
+
const state = p.get("state") ?? undefined;
|
|
700
|
+
if (!clientId || !redirectUri) {
|
|
701
|
+
return oauthError("invalid_request", "client_id and redirect_uri are required");
|
|
702
|
+
}
|
|
703
|
+
const client = await config.clients.get(clientId);
|
|
704
|
+
if (!client)
|
|
705
|
+
return oauthError("invalid_client", "Unknown client_id", 401);
|
|
706
|
+
if (!client.redirectUris.includes(redirectUri)) {
|
|
707
|
+
return oauthError("invalid_request", "redirect_uri does not match a registered URI");
|
|
708
|
+
}
|
|
709
|
+
if (responseType !== "code") {
|
|
710
|
+
return redirectWith(redirectUri, {
|
|
711
|
+
error: "unsupported_response_type",
|
|
712
|
+
...state && { state }
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
if (!codeChallenge || codeChallengeMethod !== "S256") {
|
|
716
|
+
return redirectWith(redirectUri, {
|
|
717
|
+
error: "invalid_request",
|
|
718
|
+
error_description: "PKCE S256 code_challenge is required",
|
|
719
|
+
...state && { state }
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
if (!resource) {
|
|
723
|
+
return redirectWith(redirectUri, {
|
|
724
|
+
error: "invalid_target",
|
|
725
|
+
error_description: "resource parameter is required",
|
|
726
|
+
...state && { state }
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
if (resource !== config.resource) {
|
|
730
|
+
return redirectWith(redirectUri, {
|
|
731
|
+
error: "invalid_target",
|
|
732
|
+
error_description: "resource is not served by this authorization server",
|
|
733
|
+
...state && { state }
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
const authRequest = { clientId, redirectUri, scope, resource, state };
|
|
737
|
+
const result = await config.authorizeUser(req, authRequest);
|
|
738
|
+
if (result instanceof Response)
|
|
739
|
+
return result;
|
|
740
|
+
const code = randomUUID2();
|
|
741
|
+
await config.codes.save(code, {
|
|
742
|
+
clientId,
|
|
743
|
+
redirectUri,
|
|
744
|
+
codeChallenge,
|
|
745
|
+
codeChallengeMethod: "S256",
|
|
746
|
+
resource,
|
|
747
|
+
scope,
|
|
748
|
+
userId: result.userId,
|
|
749
|
+
expiresAt: Date.now() + AUTH_CODE_TTL_MS
|
|
750
|
+
});
|
|
751
|
+
return redirectWith(redirectUri, { code, ...state && { state } });
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
const issueAccessToken = (userId, audience, clientId, scope) => signJwt({ scope, client_id: clientId }, config.signingSecret, {
|
|
755
|
+
issuer: config.issuer,
|
|
756
|
+
audience,
|
|
757
|
+
subject: userId,
|
|
758
|
+
expiresInSec: ttl
|
|
759
|
+
});
|
|
760
|
+
const tokenRoute = {
|
|
761
|
+
method: "ALL",
|
|
762
|
+
path: tokenPath,
|
|
763
|
+
handler: async (req) => {
|
|
764
|
+
if (req.method === "OPTIONS")
|
|
765
|
+
return new Response(null, { status: 204, headers: PUBLIC_CORS2 });
|
|
766
|
+
if (req.method !== "POST")
|
|
767
|
+
return oauthError("invalid_request", "POST required", 405);
|
|
768
|
+
const form = await readForm(req);
|
|
769
|
+
const grantType = form.get("grant_type");
|
|
770
|
+
if (grantType === "authorization_code") {
|
|
771
|
+
const code = form.get("code");
|
|
772
|
+
const verifier = form.get("code_verifier");
|
|
773
|
+
const redirectUri = form.get("redirect_uri");
|
|
774
|
+
const clientId = form.get("client_id");
|
|
775
|
+
if (!code || !verifier || !redirectUri || !clientId) {
|
|
776
|
+
return oauthError("invalid_request", "code, code_verifier, redirect_uri, client_id required");
|
|
777
|
+
}
|
|
778
|
+
const data = await config.codes.take(code);
|
|
779
|
+
if (!data)
|
|
780
|
+
return oauthError("invalid_grant", "Unknown or used authorization code");
|
|
781
|
+
if (data.expiresAt < Date.now())
|
|
782
|
+
return oauthError("invalid_grant", "Authorization code expired");
|
|
783
|
+
if (data.clientId !== clientId)
|
|
784
|
+
return oauthError("invalid_grant", "client_id mismatch");
|
|
785
|
+
if (data.redirectUri !== redirectUri)
|
|
786
|
+
return oauthError("invalid_grant", "redirect_uri mismatch");
|
|
787
|
+
if (!await verifyPkce(verifier, data.codeChallenge)) {
|
|
788
|
+
return oauthError("invalid_grant", "PKCE verification failed");
|
|
789
|
+
}
|
|
790
|
+
const accessToken = await issueAccessToken(data.userId, data.resource, data.clientId, data.scope);
|
|
791
|
+
let refreshToken;
|
|
792
|
+
if (config.refreshTokens) {
|
|
793
|
+
refreshToken = randomUUID2();
|
|
794
|
+
await config.refreshTokens.save(refreshToken, {
|
|
795
|
+
clientId: data.clientId,
|
|
796
|
+
resource: data.resource,
|
|
797
|
+
scope: data.scope,
|
|
798
|
+
userId: data.userId
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
return json({
|
|
802
|
+
access_token: accessToken,
|
|
803
|
+
token_type: "Bearer",
|
|
804
|
+
expires_in: ttl,
|
|
805
|
+
...data.scope && { scope: data.scope },
|
|
806
|
+
...refreshToken && { refresh_token: refreshToken }
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
if (grantType === "refresh_token") {
|
|
810
|
+
if (!config.refreshTokens) {
|
|
811
|
+
return oauthError("unsupported_grant_type", "refresh_token grant is not enabled");
|
|
812
|
+
}
|
|
813
|
+
const token = form.get("refresh_token");
|
|
814
|
+
const clientId = form.get("client_id");
|
|
815
|
+
if (!token || !clientId)
|
|
816
|
+
return oauthError("invalid_request", "refresh_token and client_id required");
|
|
817
|
+
const data = await config.refreshTokens.take(token);
|
|
818
|
+
if (!data)
|
|
819
|
+
return oauthError("invalid_grant", "Unknown or used refresh token");
|
|
820
|
+
if (data.clientId !== clientId)
|
|
821
|
+
return oauthError("invalid_grant", "client_id mismatch");
|
|
822
|
+
const accessToken = await issueAccessToken(data.userId, data.resource, data.clientId, data.scope);
|
|
823
|
+
const newRefresh = randomUUID2();
|
|
824
|
+
await config.refreshTokens.save(newRefresh, data);
|
|
825
|
+
return json({
|
|
826
|
+
access_token: accessToken,
|
|
827
|
+
token_type: "Bearer",
|
|
828
|
+
expires_in: ttl,
|
|
829
|
+
...data.scope && { scope: data.scope },
|
|
830
|
+
refresh_token: newRefresh
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
return oauthError("unsupported_grant_type", `Unsupported grant_type: ${grantType ?? "none"}`);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
return [metadataRoute, registerRoute, authorizeRoute, tokenRoute];
|
|
837
|
+
}
|
|
616
838
|
// src/tools/remote.ts
|
|
617
839
|
function toArgs(ctx) {
|
|
618
840
|
const { params, input } = ctx;
|
|
@@ -632,6 +854,8 @@ function implementRemote(contract, http, options) {
|
|
|
632
854
|
desc: endpoint.desc,
|
|
633
855
|
toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
|
|
634
856
|
expose: endpoint.expose,
|
|
857
|
+
ui: "ui" in endpoint ? endpoint.ui : undefined,
|
|
858
|
+
annotations: "annotations" in endpoint ? endpoint.annotations : undefined,
|
|
635
859
|
scope: endpoint.scope ?? groupScope,
|
|
636
860
|
paramsSchema: endpoint.params,
|
|
637
861
|
inputSchema: endpoint.input,
|
|
@@ -643,7 +867,15 @@ function implementRemote(contract, http, options) {
|
|
|
643
867
|
throw new Error(`implementRemote: endpoint "${key}" is not exposed over HTTP`);
|
|
644
868
|
}
|
|
645
869
|
const args = toArgs(ctx);
|
|
646
|
-
|
|
870
|
+
const finalArgs = options?.transformArgs ? await options.transformArgs(key, args) : args;
|
|
871
|
+
try {
|
|
872
|
+
return await call(finalArgs);
|
|
873
|
+
} catch (err) {
|
|
874
|
+
if (ApiError.is(err)) {
|
|
875
|
+
throw new AppError(err.code, err.message, err.status, isRecord(err.details) ? err.details : undefined, err.hint);
|
|
876
|
+
}
|
|
877
|
+
throw err;
|
|
878
|
+
}
|
|
647
879
|
}
|
|
648
880
|
};
|
|
649
881
|
}
|
|
@@ -654,12 +886,21 @@ function implementRemote(contract, http, options) {
|
|
|
654
886
|
methods
|
|
655
887
|
};
|
|
656
888
|
}
|
|
889
|
+
// src/tools/toolkit.ts
|
|
890
|
+
function createToolkit() {
|
|
891
|
+
return {
|
|
892
|
+
mountMcp: (server, services, config) => mountMcp(server, services, config),
|
|
893
|
+
mountAgent: (services, config) => mountAgent(services, config),
|
|
894
|
+
buildMcpServer: (config, auth) => buildMcpServer(config, auth),
|
|
895
|
+
createMcpHandler: (config) => createMcpHandler(config),
|
|
896
|
+
createStdioMcpServer: (config) => createStdioMcpServer(config),
|
|
897
|
+
createCli: (config) => createCli(config)
|
|
898
|
+
};
|
|
899
|
+
}
|
|
657
900
|
// src/tools/view-file.ts
|
|
658
|
-
import {
|
|
659
|
-
import {
|
|
660
|
-
import {
|
|
661
|
-
import { extname, resolve, sep } from "node:path";
|
|
662
|
-
import { z as z7 } from "zod";
|
|
901
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
902
|
+
import { extname as extname2, resolve } from "node:path";
|
|
903
|
+
import { z as z3 } from "zod";
|
|
663
904
|
var MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
664
905
|
var EXT_MIME = {
|
|
665
906
|
".png": "image/png",
|
|
@@ -675,103 +916,50 @@ var EXT_MIME = {
|
|
|
675
916
|
".webm": "video/webm",
|
|
676
917
|
".mov": "video/quicktime"
|
|
677
918
|
};
|
|
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
919
|
async function fetchSource(pathOrUrl, options) {
|
|
739
|
-
const extMime = EXT_MIME[
|
|
920
|
+
const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
|
|
740
921
|
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
741
922
|
const url = new URL(pathOrUrl);
|
|
742
|
-
|
|
743
|
-
await assertPublicUrl(url);
|
|
744
|
-
const res = await fetch(url);
|
|
923
|
+
const res = await fetchGuarded(url, options.allowPrivateHosts ?? false);
|
|
745
924
|
if (!res.ok)
|
|
746
925
|
throw new Error(`HTTP ${res.status}`);
|
|
747
926
|
const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
|
|
748
|
-
const
|
|
927
|
+
const mimeType = headerMime || extMime || "application/octet-stream";
|
|
749
928
|
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
750
929
|
if (declared > MAX_INLINE_BYTES) {
|
|
751
930
|
await res.body?.cancel();
|
|
752
|
-
return { tooLarge: true, mimeType
|
|
931
|
+
return { tooLarge: true, mimeType };
|
|
753
932
|
}
|
|
754
933
|
const buffer = await readCapped(res, MAX_INLINE_BYTES);
|
|
755
|
-
return buffer ? { buffer, mimeType
|
|
934
|
+
return buffer ? { buffer, mimeType } : { tooLarge: true, mimeType };
|
|
756
935
|
}
|
|
757
936
|
if (!options.baseDir) {
|
|
758
937
|
throw new Error("local file paths are disabled — set baseDir to allow them");
|
|
759
938
|
}
|
|
939
|
+
if (!extMime) {
|
|
940
|
+
throw new Error("refusing to read a non-media file");
|
|
941
|
+
}
|
|
760
942
|
const root = resolve(options.baseDir);
|
|
761
943
|
const target = resolve(root, pathOrUrl);
|
|
762
|
-
if (
|
|
944
|
+
if (!isWithinDir(root, target)) {
|
|
763
945
|
throw new Error("path escapes the allowed directory");
|
|
764
946
|
}
|
|
765
|
-
const
|
|
947
|
+
const realTarget = await realpath(target).catch(() => null);
|
|
948
|
+
if (realTarget === null)
|
|
949
|
+
throw new Error("file not found");
|
|
950
|
+
const realRoot = await realpath(root).catch(() => root);
|
|
951
|
+
if (!isWithinDir(realRoot, realTarget)) {
|
|
952
|
+
throw new Error("path escapes the allowed directory");
|
|
953
|
+
}
|
|
954
|
+
const info = await stat(realTarget).catch(() => null);
|
|
766
955
|
if (!info?.isFile())
|
|
767
956
|
throw new Error("file not found");
|
|
768
|
-
const mimeType = extMime ?? "application/octet-stream";
|
|
769
957
|
if (info.size > MAX_INLINE_BYTES)
|
|
770
|
-
return { tooLarge: true, mimeType };
|
|
771
|
-
return { buffer: await readFile(
|
|
958
|
+
return { tooLarge: true, mimeType: extMime };
|
|
959
|
+
return { buffer: await readFile(realTarget), mimeType: extMime };
|
|
772
960
|
}
|
|
773
961
|
async function resolveMedia(pathOrUrl, options = {}) {
|
|
774
|
-
const extMime = EXT_MIME[
|
|
962
|
+
const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
|
|
775
963
|
if (extMime?.startsWith("video/")) {
|
|
776
964
|
return [{ type: "text", text: `[video] ${extMime} — ${pathOrUrl}` }];
|
|
777
965
|
}
|
|
@@ -786,13 +974,23 @@ async function resolveMedia(pathOrUrl, options = {}) {
|
|
|
786
974
|
}
|
|
787
975
|
if (mimeType.startsWith("image/")) {
|
|
788
976
|
return [
|
|
789
|
-
{
|
|
977
|
+
{
|
|
978
|
+
type: "image",
|
|
979
|
+
data: buffer.toString("base64"),
|
|
980
|
+
mimeType,
|
|
981
|
+
annotations: { audience: ["user", "assistant"], priority: 0.9 }
|
|
982
|
+
},
|
|
790
983
|
{ type: "text", text: `[image] ${mimeType}, ${sizeKb}KB` }
|
|
791
984
|
];
|
|
792
985
|
}
|
|
793
986
|
if (mimeType.startsWith("audio/")) {
|
|
794
987
|
return [
|
|
795
|
-
{
|
|
988
|
+
{
|
|
989
|
+
type: "audio",
|
|
990
|
+
data: buffer.toString("base64"),
|
|
991
|
+
mimeType,
|
|
992
|
+
annotations: { audience: ["user", "assistant"], priority: 0.9 }
|
|
993
|
+
},
|
|
796
994
|
{ type: "text", text: `[audio] ${mimeType}, ${sizeKb}KB` }
|
|
797
995
|
];
|
|
798
996
|
}
|
|
@@ -802,8 +1000,9 @@ function mountViewFile(server, options = {}) {
|
|
|
802
1000
|
server.registerTool("view_file", {
|
|
803
1001
|
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
1002
|
inputSchema: {
|
|
805
|
-
paths:
|
|
806
|
-
}
|
|
1003
|
+
paths: z3.union([z3.string(), z3.array(z3.string())]).describe("Media URL(s) or file path(s) to view")
|
|
1004
|
+
},
|
|
1005
|
+
annotations: { title: "View Media", readOnlyHint: true, idempotentHint: true }
|
|
807
1006
|
}, async (args) => {
|
|
808
1007
|
const list = Array.isArray(args.paths) ? args.paths : [args.paths];
|
|
809
1008
|
const content = [];
|
|
@@ -821,17 +1020,31 @@ function mountViewFile(server, options = {}) {
|
|
|
821
1020
|
});
|
|
822
1021
|
}
|
|
823
1022
|
export {
|
|
824
|
-
|
|
1023
|
+
wwwAuthenticateHeader,
|
|
825
1024
|
validateMcpSchemas,
|
|
826
1025
|
resolveMedia,
|
|
1026
|
+
protectedResourceMetadataUrl,
|
|
1027
|
+
oauthProtectedResourceRoute,
|
|
1028
|
+
mountWait,
|
|
827
1029
|
mountViewFile,
|
|
1030
|
+
mountUpload,
|
|
1031
|
+
mountOAuthProvider,
|
|
1032
|
+
mountMcpResource,
|
|
828
1033
|
mountMcp,
|
|
1034
|
+
mountDownload,
|
|
829
1035
|
mountAgent,
|
|
1036
|
+
inlineMcpAppBundle,
|
|
830
1037
|
implementRemote,
|
|
831
1038
|
flattenDiscriminatedUnion,
|
|
1039
|
+
createToolkit,
|
|
832
1040
|
createStdioMcpServer,
|
|
833
1041
|
createMcpHandler,
|
|
1042
|
+
createCli,
|
|
834
1043
|
collectTools,
|
|
1044
|
+
coerceJsonArgs,
|
|
835
1045
|
buildToolManifest,
|
|
836
|
-
buildMcpServer
|
|
1046
|
+
buildMcpServer,
|
|
1047
|
+
RESOURCE_MIME_TYPE,
|
|
1048
|
+
PROTECTED_RESOURCE_PATH,
|
|
1049
|
+
EXT_APPS_BUNDLE_PLACEHOLDER
|
|
837
1050
|
};
|