stitchkit 0.2.0 → 0.4.0

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