stitchkit 0.3.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/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-dpj1jcys.js → index-3qmtdfpa.js} +156 -4
- package/dist/index-48ffdxgk.js +6 -0
- package/dist/{index-4kz3aqpx.js → index-78q1qm7v.js} +4 -1
- package/dist/{index-kckky6zw.js → index-afzt3nmx.js} +1 -1
- package/dist/{index-4b1j82gp.js → index-d4rwrjbc.js} +1 -1
- package/dist/{index-0t460v7k.js → index-vhdvv00d.js} +1 -1
- package/dist/index-x3fcszf8.js +8 -0
- package/dist/index.js +16 -4
- package/dist/internal/http-input.d.ts +13 -0
- package/dist/internal/http-input.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.map +1 -1
- package/dist/node.d.ts +10 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +38 -7
- package/dist/observability/audit.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 +3 -0
- package/dist/react.js +2 -0
- package/dist/server/implement.d.ts.map +1 -1
- package/dist/server/index.d.ts +5 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +172 -216
- package/dist/server/middleware/auth.d.ts +17 -0
- package/dist/server/middleware/auth.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/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/socket-io.d.ts +60 -15
- package/dist/server/socket-io.d.ts.map +1 -1
- package/dist/server/types.d.ts +5 -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.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/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 +6 -2
- 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 +12 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +587 -421
- package/package.json +22 -9
- package/dist/index-q6ja2qwq.js +0 -7
package/dist/tools.js
CHANGED
|
@@ -1,314 +1,42 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ApiError,
|
|
2
3
|
createClient
|
|
3
|
-
} from "./index-
|
|
4
|
+
} from "./index-78q1qm7v.js";
|
|
4
5
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
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";
|
|
7
17
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
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";
|
|
13
29
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
30
|
+
AppError
|
|
31
|
+
} from "./index-afzt3nmx.js";
|
|
32
|
+
import"./index-kzfs85xp.js";
|
|
17
33
|
import {
|
|
18
34
|
isRecord
|
|
19
35
|
} from "./index-809wc1tt.js";
|
|
36
|
+
import"./index-37x76zdn.js";
|
|
20
37
|
|
|
21
38
|
// src/tools/agent.ts
|
|
22
39
|
import { tool, zodSchema } from "ai";
|
|
23
|
-
|
|
24
|
-
// src/tools/coerce.ts
|
|
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";
|
|
54
|
-
function objectShapeKeys(schema) {
|
|
55
|
-
return schema instanceof z2.ZodObject ? Object.keys(schema.shape) : [];
|
|
56
|
-
}
|
|
57
|
-
function mergeSchemas(paramsSchema, inputSchema) {
|
|
58
|
-
if (paramsSchema && !(paramsSchema instanceof z2.ZodObject)) {
|
|
59
|
-
throw new Error("Tool params schema must be a z.object()");
|
|
60
|
-
}
|
|
61
|
-
const paramsObject = paramsSchema instanceof z2.ZodObject ? paramsSchema : undefined;
|
|
62
|
-
if (!inputSchema) {
|
|
63
|
-
return paramsObject ?? z2.object({});
|
|
64
|
-
}
|
|
65
|
-
if (inputSchema instanceof z2.ZodObject) {
|
|
66
|
-
if (paramsObject) {
|
|
67
|
-
const conflicts = Object.keys(paramsObject.shape).filter((key) => (key in inputSchema.shape));
|
|
68
|
-
if (conflicts.length > 0) {
|
|
69
|
-
throw new Error(`Schema merge conflict: ${conflicts.join(", ")} appear in both params and input`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return z2.object({ ...paramsObject?.shape ?? {}, ...inputSchema.shape });
|
|
73
|
-
}
|
|
74
|
-
return paramsObject ? z2.intersection(paramsObject, inputSchema) : inputSchema;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// src/tools/execute.ts
|
|
78
|
-
function toolResultFromError(err) {
|
|
79
|
-
const appErr = normalizeError(err);
|
|
80
|
-
return {
|
|
81
|
-
ok: false,
|
|
82
|
-
code: appErr.code,
|
|
83
|
-
details: appErr.details ?? { message: appErr.message },
|
|
84
|
-
...appErr.hint && { hint: appErr.hint }
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
async function executeToolMethod(method, toolName, rawArgs, context, hooks, lifecycle, coerceJson = false) {
|
|
88
|
-
const startedAt = Date.now();
|
|
89
|
-
const finish = async (result) => {
|
|
90
|
-
await hooks?.afterToolCall?.(toolName, rawArgs, result, Date.now() - startedAt, context);
|
|
91
|
-
return result;
|
|
92
|
-
};
|
|
93
|
-
if (hooks?.beforeToolCall) {
|
|
94
|
-
try {
|
|
95
|
-
await hooks.beforeToolCall(toolName, rawArgs, context);
|
|
96
|
-
} catch (err) {
|
|
97
|
-
return finish(toolResultFromError(err));
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const paramKeys = new Set(objectShapeKeys(method.paramsSchema));
|
|
101
|
-
let paramArgs = {};
|
|
102
|
-
let inputArgs = {};
|
|
103
|
-
for (const [key, value] of Object.entries(rawArgs)) {
|
|
104
|
-
if (isUnsafeKey(key))
|
|
105
|
-
continue;
|
|
106
|
-
if (paramKeys.has(key))
|
|
107
|
-
paramArgs[key] = value;
|
|
108
|
-
else
|
|
109
|
-
inputArgs[key] = value;
|
|
110
|
-
}
|
|
111
|
-
if (coerceJson) {
|
|
112
|
-
paramArgs = coerceJsonArgs(paramArgs, method.paramsSchema);
|
|
113
|
-
inputArgs = coerceJsonArgs(inputArgs, method.inputSchema);
|
|
114
|
-
}
|
|
115
|
-
let params;
|
|
116
|
-
if (method.paramsSchema) {
|
|
117
|
-
const result = method.paramsSchema.safeParse(paramArgs);
|
|
118
|
-
if (!result.success) {
|
|
119
|
-
return finish({
|
|
120
|
-
ok: false,
|
|
121
|
-
code: "VALIDATION_ERROR",
|
|
122
|
-
details: { message: `Invalid params: ${formatZodError(result.error)}` }
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
params = result.data;
|
|
126
|
-
}
|
|
127
|
-
let input;
|
|
128
|
-
if (method.inputSchema) {
|
|
129
|
-
const result = method.inputSchema.safeParse(inputArgs);
|
|
130
|
-
if (!result.success) {
|
|
131
|
-
return finish({
|
|
132
|
-
ok: false,
|
|
133
|
-
code: "VALIDATION_ERROR",
|
|
134
|
-
details: { message: `Invalid input: ${formatZodError(result.error)}` }
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
input = result.data;
|
|
138
|
-
}
|
|
139
|
-
try {
|
|
140
|
-
const ctx = { ...context, params, input, source: context.source };
|
|
141
|
-
if (lifecycle?.beforeHandle) {
|
|
142
|
-
await lifecycle.beforeHandle(ctx, method);
|
|
143
|
-
}
|
|
144
|
-
let data = await method.handler(ctx);
|
|
145
|
-
if (lifecycle?.afterHandle) {
|
|
146
|
-
const transformed = await lifecycle.afterHandle(ctx, data, method);
|
|
147
|
-
if (transformed !== undefined)
|
|
148
|
-
data = transformed;
|
|
149
|
-
}
|
|
150
|
-
if (method.outputSchema) {
|
|
151
|
-
const checked = validateHandlerOutput(method.outputSchema, data);
|
|
152
|
-
if (!checked.ok) {
|
|
153
|
-
return finish({
|
|
154
|
-
ok: false,
|
|
155
|
-
code: "INTERNAL_SERVER_ERROR",
|
|
156
|
-
details: { message: checked.message }
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
data = checked.data;
|
|
160
|
-
}
|
|
161
|
-
const output = (data === undefined || data === null) && !method.outputSchema ? { status: "ok" } : data;
|
|
162
|
-
return finish({ ok: true, data: output });
|
|
163
|
-
} catch (err) {
|
|
164
|
-
return finish(toolResultFromError(err));
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// src/tools/mount.ts
|
|
169
|
-
import { z as z4 } from "zod";
|
|
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.describe(hint);
|
|
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, 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
|
-
const shouldExtend = !!extend && (!extend.filter || extend.filter(service, method));
|
|
278
|
-
const schema = shouldExtend && extend ? applyExtend(baseSchema, extend.schema) : baseSchema;
|
|
279
|
-
tools.push({ method, name, schema, shouldExtend });
|
|
280
|
-
}
|
|
281
|
-
return tools;
|
|
282
|
-
}
|
|
283
|
-
function createToolRunner(config) {
|
|
284
|
-
const extendKeys = config.extend ? new Set(Object.keys(config.extend.schema)) : null;
|
|
285
|
-
return async (tool, rawArgs) => {
|
|
286
|
-
let extraContext = {};
|
|
287
|
-
if (tool.shouldExtend && config.extend) {
|
|
288
|
-
extraContext = await config.extend.resolve(rawArgs);
|
|
289
|
-
}
|
|
290
|
-
const cleanArgs = extendKeys ? Object.fromEntries(Object.entries(rawArgs).filter(([key]) => !extendKeys.has(key))) : rawArgs;
|
|
291
|
-
return executeToolMethod(tool.method, tool.name, cleanArgs, { ...config.context, ...extraContext, source: config.source }, config.hooks, config.lifecycle, config.coerceJsonArgs ?? true);
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
function formatToolError(result, toolName, errorHint) {
|
|
295
|
-
const err = { error: result.code };
|
|
296
|
-
if (result.details)
|
|
297
|
-
err.details = result.details;
|
|
298
|
-
const hints = [];
|
|
299
|
-
if (result.hint)
|
|
300
|
-
hints.push(result.hint);
|
|
301
|
-
if (errorHint && toolName) {
|
|
302
|
-
const global = errorHint(toolName, result.code);
|
|
303
|
-
if (global)
|
|
304
|
-
hints.push(global);
|
|
305
|
-
}
|
|
306
|
-
if (hints.length > 0)
|
|
307
|
-
err._hint = hints.join(" ");
|
|
308
|
-
return err;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// src/tools/agent.ts
|
|
312
40
|
function mountAgent(services, config = {}) {
|
|
313
41
|
const serviceList = Array.isArray(services) ? services : [services];
|
|
314
42
|
const tools = {};
|
|
@@ -348,17 +76,6 @@ function mountAgent(services, config = {}) {
|
|
|
348
76
|
}
|
|
349
77
|
return tools;
|
|
350
78
|
}
|
|
351
|
-
// src/tools/json-schema.ts
|
|
352
|
-
import { z as z5 } from "zod";
|
|
353
|
-
function toJsonSchema(schema, io) {
|
|
354
|
-
return z5.toJSONSchema(schema, {
|
|
355
|
-
io,
|
|
356
|
-
target: "draft-2020-12",
|
|
357
|
-
unrepresentable: "throw",
|
|
358
|
-
cycles: "ref"
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
|
|
362
79
|
// src/tools/manifest.ts
|
|
363
80
|
function buildToolManifest(tools) {
|
|
364
81
|
return tools.map((t) => {
|
|
@@ -373,7 +90,31 @@ function buildToolManifest(tools) {
|
|
|
373
90
|
}
|
|
374
91
|
// src/tools/mcp.ts
|
|
375
92
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
376
|
-
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
|
|
377
118
|
function textBlock(text) {
|
|
378
119
|
return [{ type: "text", text }];
|
|
379
120
|
}
|
|
@@ -404,10 +145,10 @@ function probeSchema(schema, io) {
|
|
|
404
145
|
function resolveOutputSchema(outputSchema) {
|
|
405
146
|
if (!outputSchema)
|
|
406
147
|
return null;
|
|
407
|
-
if (outputSchema instanceof
|
|
148
|
+
if (outputSchema instanceof z.ZodObject) {
|
|
408
149
|
return { schema: outputSchema, mode: "direct" };
|
|
409
150
|
}
|
|
410
|
-
return { schema:
|
|
151
|
+
return { schema: z.object({ result: outputSchema }), mode: "wrapped" };
|
|
411
152
|
}
|
|
412
153
|
function reportIncompatible(message, policy, logger, failures) {
|
|
413
154
|
if (policy === "throw") {
|
|
@@ -431,7 +172,7 @@ function prepareMcpTool(mountable, policy, logger, failures, seen) {
|
|
|
431
172
|
throw new Error(`Duplicate MCP tool name "${mountable.name}" across mounted services`);
|
|
432
173
|
}
|
|
433
174
|
seen.add(mountable.name);
|
|
434
|
-
if (!(mountable.schema instanceof
|
|
175
|
+
if (!(mountable.schema instanceof z.ZodObject)) {
|
|
435
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);
|
|
436
177
|
return null;
|
|
437
178
|
}
|
|
@@ -485,6 +226,15 @@ function mountMcp(mcpServer, services, config = {}) {
|
|
|
485
226
|
const toolConfig = { description: mountable.method.desc, inputSchema: mountable.schema };
|
|
486
227
|
if (prepared.outputSchema)
|
|
487
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
|
+
}
|
|
488
238
|
mcpServer.registerTool(mountable.name, toolConfig, async (rawArgs) => {
|
|
489
239
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
490
240
|
try {
|
|
@@ -513,13 +263,70 @@ function buildMcpServer(config, auth) {
|
|
|
513
263
|
errorHint: config.errorHint
|
|
514
264
|
});
|
|
515
265
|
config.nativeTools?.(server);
|
|
266
|
+
for (const resource of config.resources ?? []) {
|
|
267
|
+
mountMcpResource(server, resource);
|
|
268
|
+
}
|
|
516
269
|
return server;
|
|
517
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
|
+
}
|
|
518
283
|
// src/tools/mcp-handler.ts
|
|
519
284
|
import { randomUUID } from "node:crypto";
|
|
520
285
|
import {
|
|
521
286
|
WebStandardStreamableHTTPServerTransport
|
|
522
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
|
|
523
330
|
var EVENT_TTL_MS = 10 * 60 * 1000;
|
|
524
331
|
var SESSION_TTL_MS = 30 * 60 * 1000;
|
|
525
332
|
var SWEEP_INTERVAL_MS = 60 * 1000;
|
|
@@ -562,13 +369,31 @@ class InMemoryEventStore {
|
|
|
562
369
|
}
|
|
563
370
|
}
|
|
564
371
|
}
|
|
565
|
-
function jsonRpcError(code, message, status) {
|
|
566
|
-
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 });
|
|
567
374
|
}
|
|
568
375
|
function createMcpHandler(config) {
|
|
569
376
|
if (Array.isArray(config.services)) {
|
|
570
377
|
validateMcpSchemas(config.services, config.onIncompatibleSchema, config.logger);
|
|
571
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
|
+
}
|
|
572
397
|
const eventStore = new InMemoryEventStore;
|
|
573
398
|
const sessions = new Map;
|
|
574
399
|
const closeTransport = (transport) => {
|
|
@@ -588,9 +413,8 @@ function createMcpHandler(config) {
|
|
|
588
413
|
}, SWEEP_INTERVAL_MS).unref();
|
|
589
414
|
return async (req) => {
|
|
590
415
|
const auth = await config.auth(req);
|
|
591
|
-
if (!auth)
|
|
592
|
-
return
|
|
593
|
-
}
|
|
416
|
+
if (!auth)
|
|
417
|
+
return unauthorized();
|
|
594
418
|
const sessionId = req.headers.get("mcp-session-id");
|
|
595
419
|
if (sessionId) {
|
|
596
420
|
const existing = sessions.get(sessionId);
|
|
@@ -641,6 +465,376 @@ async function createStdioMcpServer(config) {
|
|
|
641
465
|
await server.connect(new StdioServerTransport);
|
|
642
466
|
return server;
|
|
643
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
|
+
}
|
|
644
838
|
// src/tools/remote.ts
|
|
645
839
|
function toArgs(ctx) {
|
|
646
840
|
const { params, input } = ctx;
|
|
@@ -660,6 +854,8 @@ function implementRemote(contract, http, options) {
|
|
|
660
854
|
desc: endpoint.desc,
|
|
661
855
|
toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
|
|
662
856
|
expose: endpoint.expose,
|
|
857
|
+
ui: "ui" in endpoint ? endpoint.ui : undefined,
|
|
858
|
+
annotations: "annotations" in endpoint ? endpoint.annotations : undefined,
|
|
663
859
|
scope: endpoint.scope ?? groupScope,
|
|
664
860
|
paramsSchema: endpoint.params,
|
|
665
861
|
inputSchema: endpoint.input,
|
|
@@ -671,7 +867,15 @@ function implementRemote(contract, http, options) {
|
|
|
671
867
|
throw new Error(`implementRemote: endpoint "${key}" is not exposed over HTTP`);
|
|
672
868
|
}
|
|
673
869
|
const args = toArgs(ctx);
|
|
674
|
-
|
|
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
|
+
}
|
|
675
879
|
}
|
|
676
880
|
};
|
|
677
881
|
}
|
|
@@ -682,14 +886,21 @@ function implementRemote(contract, http, options) {
|
|
|
682
886
|
methods
|
|
683
887
|
};
|
|
684
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
|
+
}
|
|
685
900
|
// src/tools/view-file.ts
|
|
686
|
-
import {
|
|
687
|
-
import {
|
|
688
|
-
import {
|
|
689
|
-
import { extname, resolve } from "node:path";
|
|
690
|
-
import { z as z7 } from "zod";
|
|
691
|
-
var NUMERIC_HOST = /^(0x[0-9a-f]+|[0-9.]+)$/i;
|
|
692
|
-
var MAX_REDIRECTS = 5;
|
|
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";
|
|
693
904
|
var MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
694
905
|
var EXT_MIME = {
|
|
695
906
|
".png": "image/png",
|
|
@@ -705,120 +916,50 @@ var EXT_MIME = {
|
|
|
705
916
|
".webm": "video/webm",
|
|
706
917
|
".mov": "video/quicktime"
|
|
707
918
|
};
|
|
708
|
-
function isPrivateIp(ip) {
|
|
709
|
-
const family = isIP(ip);
|
|
710
|
-
if (family === 4) {
|
|
711
|
-
const [a, b] = ip.split(".").map(Number);
|
|
712
|
-
if (a === undefined || b === undefined)
|
|
713
|
-
return true;
|
|
714
|
-
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;
|
|
715
|
-
}
|
|
716
|
-
if (family === 6) {
|
|
717
|
-
const lower = ip.toLowerCase();
|
|
718
|
-
if (lower === "::1" || lower === "::")
|
|
719
|
-
return true;
|
|
720
|
-
if (lower.startsWith("fe8") || lower.startsWith("fe9") || lower.startsWith("fea") || lower.startsWith("feb") || lower.startsWith("fc") || lower.startsWith("fd")) {
|
|
721
|
-
return true;
|
|
722
|
-
}
|
|
723
|
-
const mapped = lower.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
724
|
-
return mapped?.[1] ? isPrivateIp(mapped[1]) : false;
|
|
725
|
-
}
|
|
726
|
-
return false;
|
|
727
|
-
}
|
|
728
|
-
async function assertPublicUrl(url) {
|
|
729
|
-
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
730
|
-
if (isIP(host)) {
|
|
731
|
-
if (isPrivateIp(host))
|
|
732
|
-
throw new Error("refusing to fetch a private address");
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
if (NUMERIC_HOST.test(host)) {
|
|
736
|
-
throw new Error("refusing to fetch a non-canonical numeric host");
|
|
737
|
-
}
|
|
738
|
-
if (host === "localhost" || host.endsWith(".local") || host.endsWith(".internal")) {
|
|
739
|
-
throw new Error("refusing to fetch an internal host");
|
|
740
|
-
}
|
|
741
|
-
const records = await lookup(host, { all: true });
|
|
742
|
-
for (const record of records) {
|
|
743
|
-
if (isPrivateIp(record.address)) {
|
|
744
|
-
throw new Error("refusing to fetch a host that resolves to a private address");
|
|
745
|
-
}
|
|
746
|
-
}
|
|
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
|
-
}
|
|
764
|
-
async function readCapped(res, max) {
|
|
765
|
-
const reader = res.body?.getReader();
|
|
766
|
-
if (!reader)
|
|
767
|
-
return Buffer.alloc(0);
|
|
768
|
-
const chunks = [];
|
|
769
|
-
let total = 0;
|
|
770
|
-
try {
|
|
771
|
-
while (true) {
|
|
772
|
-
const { done, value } = await reader.read();
|
|
773
|
-
if (done)
|
|
774
|
-
break;
|
|
775
|
-
total += value.length;
|
|
776
|
-
if (total > max) {
|
|
777
|
-
await reader.cancel();
|
|
778
|
-
return null;
|
|
779
|
-
}
|
|
780
|
-
chunks.push(value);
|
|
781
|
-
}
|
|
782
|
-
} finally {
|
|
783
|
-
reader.releaseLock();
|
|
784
|
-
}
|
|
785
|
-
return Buffer.concat(chunks);
|
|
786
|
-
}
|
|
787
919
|
async function fetchSource(pathOrUrl, options) {
|
|
788
|
-
const extMime = EXT_MIME[
|
|
920
|
+
const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
|
|
789
921
|
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
790
922
|
const url = new URL(pathOrUrl);
|
|
791
923
|
const res = await fetchGuarded(url, options.allowPrivateHosts ?? false);
|
|
792
924
|
if (!res.ok)
|
|
793
925
|
throw new Error(`HTTP ${res.status}`);
|
|
794
926
|
const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
|
|
795
|
-
const
|
|
927
|
+
const mimeType = headerMime || extMime || "application/octet-stream";
|
|
796
928
|
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
797
929
|
if (declared > MAX_INLINE_BYTES) {
|
|
798
930
|
await res.body?.cancel();
|
|
799
|
-
return { tooLarge: true, mimeType
|
|
931
|
+
return { tooLarge: true, mimeType };
|
|
800
932
|
}
|
|
801
933
|
const buffer = await readCapped(res, MAX_INLINE_BYTES);
|
|
802
|
-
return buffer ? { buffer, mimeType
|
|
934
|
+
return buffer ? { buffer, mimeType } : { tooLarge: true, mimeType };
|
|
803
935
|
}
|
|
804
936
|
if (!options.baseDir) {
|
|
805
937
|
throw new Error("local file paths are disabled — set baseDir to allow them");
|
|
806
938
|
}
|
|
939
|
+
if (!extMime) {
|
|
940
|
+
throw new Error("refusing to read a non-media file");
|
|
941
|
+
}
|
|
807
942
|
const root = resolve(options.baseDir);
|
|
808
943
|
const target = resolve(root, pathOrUrl);
|
|
809
944
|
if (!isWithinDir(root, target)) {
|
|
810
945
|
throw new Error("path escapes the allowed directory");
|
|
811
946
|
}
|
|
812
|
-
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);
|
|
813
955
|
if (!info?.isFile())
|
|
814
956
|
throw new Error("file not found");
|
|
815
|
-
const mimeType = extMime ?? "application/octet-stream";
|
|
816
957
|
if (info.size > MAX_INLINE_BYTES)
|
|
817
|
-
return { tooLarge: true, mimeType };
|
|
818
|
-
return { buffer: await readFile(
|
|
958
|
+
return { tooLarge: true, mimeType: extMime };
|
|
959
|
+
return { buffer: await readFile(realTarget), mimeType: extMime };
|
|
819
960
|
}
|
|
820
961
|
async function resolveMedia(pathOrUrl, options = {}) {
|
|
821
|
-
const extMime = EXT_MIME[
|
|
962
|
+
const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
|
|
822
963
|
if (extMime?.startsWith("video/")) {
|
|
823
964
|
return [{ type: "text", text: `[video] ${extMime} — ${pathOrUrl}` }];
|
|
824
965
|
}
|
|
@@ -833,13 +974,23 @@ async function resolveMedia(pathOrUrl, options = {}) {
|
|
|
833
974
|
}
|
|
834
975
|
if (mimeType.startsWith("image/")) {
|
|
835
976
|
return [
|
|
836
|
-
{
|
|
977
|
+
{
|
|
978
|
+
type: "image",
|
|
979
|
+
data: buffer.toString("base64"),
|
|
980
|
+
mimeType,
|
|
981
|
+
annotations: { audience: ["user", "assistant"], priority: 0.9 }
|
|
982
|
+
},
|
|
837
983
|
{ type: "text", text: `[image] ${mimeType}, ${sizeKb}KB` }
|
|
838
984
|
];
|
|
839
985
|
}
|
|
840
986
|
if (mimeType.startsWith("audio/")) {
|
|
841
987
|
return [
|
|
842
|
-
{
|
|
988
|
+
{
|
|
989
|
+
type: "audio",
|
|
990
|
+
data: buffer.toString("base64"),
|
|
991
|
+
mimeType,
|
|
992
|
+
annotations: { audience: ["user", "assistant"], priority: 0.9 }
|
|
993
|
+
},
|
|
843
994
|
{ type: "text", text: `[audio] ${mimeType}, ${sizeKb}KB` }
|
|
844
995
|
];
|
|
845
996
|
}
|
|
@@ -849,8 +1000,9 @@ function mountViewFile(server, options = {}) {
|
|
|
849
1000
|
server.registerTool("view_file", {
|
|
850
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.",
|
|
851
1002
|
inputSchema: {
|
|
852
|
-
paths:
|
|
853
|
-
}
|
|
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 }
|
|
854
1006
|
}, async (args) => {
|
|
855
1007
|
const list = Array.isArray(args.paths) ? args.paths : [args.paths];
|
|
856
1008
|
const content = [];
|
|
@@ -868,17 +1020,31 @@ function mountViewFile(server, options = {}) {
|
|
|
868
1020
|
});
|
|
869
1021
|
}
|
|
870
1022
|
export {
|
|
1023
|
+
wwwAuthenticateHeader,
|
|
871
1024
|
validateMcpSchemas,
|
|
872
1025
|
resolveMedia,
|
|
1026
|
+
protectedResourceMetadataUrl,
|
|
1027
|
+
oauthProtectedResourceRoute,
|
|
1028
|
+
mountWait,
|
|
873
1029
|
mountViewFile,
|
|
1030
|
+
mountUpload,
|
|
1031
|
+
mountOAuthProvider,
|
|
1032
|
+
mountMcpResource,
|
|
874
1033
|
mountMcp,
|
|
1034
|
+
mountDownload,
|
|
875
1035
|
mountAgent,
|
|
1036
|
+
inlineMcpAppBundle,
|
|
876
1037
|
implementRemote,
|
|
877
1038
|
flattenDiscriminatedUnion,
|
|
1039
|
+
createToolkit,
|
|
878
1040
|
createStdioMcpServer,
|
|
879
1041
|
createMcpHandler,
|
|
1042
|
+
createCli,
|
|
880
1043
|
collectTools,
|
|
881
1044
|
coerceJsonArgs,
|
|
882
1045
|
buildToolManifest,
|
|
883
|
-
buildMcpServer
|
|
1046
|
+
buildMcpServer,
|
|
1047
|
+
RESOURCE_MIME_TYPE,
|
|
1048
|
+
PROTECTED_RESOURCE_PATH,
|
|
1049
|
+
EXT_APPS_BUNDLE_PLACEHOLDER
|
|
884
1050
|
};
|