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.
Files changed (99) hide show
  1. package/README.md +11 -10
  2. package/dist/browser/client.d.ts.map +1 -1
  3. package/dist/browser/socket-io.d.ts +17 -0
  4. package/dist/browser/socket-io.d.ts.map +1 -1
  5. package/dist/cli.d.ts +23 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +21 -0
  8. package/dist/contract/define.d.ts +35 -2
  9. package/dist/contract/define.d.ts.map +1 -1
  10. package/dist/contract/index.d.ts +1 -1
  11. package/dist/contract/index.d.ts.map +1 -1
  12. package/dist/contract/index.js +2 -1
  13. package/dist/index-0ed3bx43.js +33 -0
  14. package/dist/index-0ma1eqv4.js +1007 -0
  15. package/dist/index-1cf8jkhf.js +219 -0
  16. package/dist/index-37x76zdn.js +4 -0
  17. package/dist/{index-dpj1jcys.js → index-3qmtdfpa.js} +156 -4
  18. package/dist/index-48ffdxgk.js +6 -0
  19. package/dist/{index-4kz3aqpx.js → index-78q1qm7v.js} +4 -1
  20. package/dist/{index-kckky6zw.js → index-afzt3nmx.js} +1 -1
  21. package/dist/{index-4b1j82gp.js → index-d4rwrjbc.js} +1 -1
  22. package/dist/{index-0t460v7k.js → index-vhdvv00d.js} +1 -1
  23. package/dist/index-x3fcszf8.js +8 -0
  24. package/dist/index.js +16 -4
  25. package/dist/internal/http-input.d.ts +13 -0
  26. package/dist/internal/http-input.d.ts.map +1 -0
  27. package/dist/internal/secure-fetch.d.ts +27 -0
  28. package/dist/internal/secure-fetch.d.ts.map +1 -0
  29. package/dist/internal/within-dir.d.ts.map +1 -1
  30. package/dist/node.d.ts +10 -1
  31. package/dist/node.d.ts.map +1 -1
  32. package/dist/node.js +38 -7
  33. package/dist/observability/audit.d.ts.map +1 -1
  34. package/dist/observability/event.d.ts +4 -0
  35. package/dist/observability/event.d.ts.map +1 -1
  36. package/dist/observability/index.js +3 -0
  37. package/dist/react.js +2 -0
  38. package/dist/server/implement.d.ts.map +1 -1
  39. package/dist/server/index.d.ts +5 -2
  40. package/dist/server/index.d.ts.map +1 -1
  41. package/dist/server/index.js +172 -216
  42. package/dist/server/middleware/auth.d.ts +17 -0
  43. package/dist/server/middleware/auth.d.ts.map +1 -1
  44. package/dist/server/middleware/pkce.d.ts +17 -0
  45. package/dist/server/middleware/pkce.d.ts.map +1 -0
  46. package/dist/server/node.d.ts +9 -0
  47. package/dist/server/node.d.ts.map +1 -1
  48. package/dist/server/openapi.d.ts +52 -0
  49. package/dist/server/openapi.d.ts.map +1 -0
  50. package/dist/server/socket-io.d.ts +60 -15
  51. package/dist/server/socket-io.d.ts.map +1 -1
  52. package/dist/server/types.d.ts +5 -1
  53. package/dist/server/types.d.ts.map +1 -1
  54. package/dist/server/websocket.d.ts +73 -0
  55. package/dist/server/websocket.d.ts.map +1 -0
  56. package/dist/tools/cli-args.d.ts +68 -0
  57. package/dist/tools/cli-args.d.ts.map +1 -0
  58. package/dist/tools/cli-format.d.ts +39 -0
  59. package/dist/tools/cli-format.d.ts.map +1 -0
  60. package/dist/tools/cli-wait.d.ts +45 -0
  61. package/dist/tools/cli-wait.d.ts.map +1 -0
  62. package/dist/tools/cli.d.ts +77 -0
  63. package/dist/tools/cli.d.ts.map +1 -0
  64. package/dist/tools/coerce.d.ts.map +1 -1
  65. package/dist/tools/json-schema.d.ts +29 -4
  66. package/dist/tools/json-schema.d.ts.map +1 -1
  67. package/dist/tools/mcp-app.d.ts +59 -0
  68. package/dist/tools/mcp-app.d.ts.map +1 -0
  69. package/dist/tools/mcp-handler.d.ts +18 -0
  70. package/dist/tools/mcp-handler.d.ts.map +1 -1
  71. package/dist/tools/mcp.d.ts +9 -0
  72. package/dist/tools/mcp.d.ts.map +1 -1
  73. package/dist/tools/mount-download.d.ts +30 -0
  74. package/dist/tools/mount-download.d.ts.map +1 -0
  75. package/dist/tools/mount-upload.d.ts +19 -0
  76. package/dist/tools/mount-upload.d.ts.map +1 -0
  77. package/dist/tools/mount-wait.d.ts +36 -0
  78. package/dist/tools/mount-wait.d.ts.map +1 -0
  79. package/dist/tools/mount.d.ts +6 -2
  80. package/dist/tools/mount.d.ts.map +1 -1
  81. package/dist/tools/native-result.d.ts +17 -0
  82. package/dist/tools/native-result.d.ts.map +1 -0
  83. package/dist/tools/oauth-metadata.d.ts +28 -0
  84. package/dist/tools/oauth-metadata.d.ts.map +1 -0
  85. package/dist/tools/oauth-provider.d.ts +85 -0
  86. package/dist/tools/oauth-provider.d.ts.map +1 -0
  87. package/dist/tools/remote.d.ts +2 -2
  88. package/dist/tools/remote.d.ts.map +1 -1
  89. package/dist/tools/toolkit.d.ts +51 -0
  90. package/dist/tools/toolkit.d.ts.map +1 -0
  91. package/dist/tools/view-file.d.ts +12 -0
  92. package/dist/tools/view-file.d.ts.map +1 -1
  93. package/dist/tools/wait-core.d.ts +33 -0
  94. package/dist/tools/wait-core.d.ts.map +1 -0
  95. package/dist/tools.d.ts +12 -1
  96. package/dist/tools.d.ts.map +1 -1
  97. package/dist/tools.js +587 -421
  98. package/package.json +22 -9
  99. 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-4kz3aqpx.js";
4
+ } from "./index-78q1qm7v.js";
4
5
  import {
5
- isWithinDir
6
- } from "./index-q6ja2qwq.js";
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
- formatZodError,
9
- normalizeError,
10
- validateHandlerOutput
11
- } from "./index-0t460v7k.js";
12
- import"./index-kckky6zw.js";
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
- isUnsafeKey,
15
- safeJsonParse
16
- } from "./index-kzfs85xp.js";
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 as z6 } from "zod";
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 z6.ZodObject) {
148
+ if (outputSchema instanceof z.ZodObject) {
408
149
  return { schema: outputSchema, mode: "direct" };
409
150
  }
410
- return { schema: z6.object({ result: outputSchema }), mode: "wrapped" };
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 z6.ZodObject)) {
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 jsonRpcError(-32001, "Authorization required", 401);
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
- return call(options?.transformArgs ? await options.transformArgs(key, args) : args);
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 { lookup } from "node:dns/promises";
687
- import { readFile, stat } from "node:fs/promises";
688
- import { isIP } from "node:net";
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[extname(pathOrUrl).toLowerCase()];
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 mimeType2 = headerMime || extMime || "application/octet-stream";
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: mimeType2 };
931
+ return { tooLarge: true, mimeType };
800
932
  }
801
933
  const buffer = await readCapped(res, MAX_INLINE_BYTES);
802
- return buffer ? { buffer, mimeType: mimeType2 } : { tooLarge: true, mimeType: mimeType2 };
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 info = await stat(target).catch(() => null);
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(target), mimeType };
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[extname(pathOrUrl).toLowerCase()];
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
- { type: "image", data: buffer.toString("base64"), mimeType },
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
- { type: "audio", data: buffer.toString("base64"), mimeType },
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: z7.union([z7.string(), z7.array(z7.string())]).describe("Media URL(s) or file path(s) to view")
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
  };