@fitzzero/quickdraw-core 5.1.1 → 5.1.3

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 (152) hide show
  1. package/README.md +1308 -219
  2. package/dist/{admin-DKwkIh5i.d.ts → admin-CS7JB_UH.d.ts} +2 -2
  3. package/dist/{adminSchemas-2DUA_hZ8.d.ts → adminSchemas-29cGgrg5.d.ts} +6 -4
  4. package/dist/{auth-BhWqdyb-.d.ts → auth-DtdYjrZ9.d.ts} +51 -21
  5. package/dist/{caller-CXTFswBO.d.ts → caller-BTwGl7vm.d.ts} +296 -33
  6. package/dist/chunk-2FDSKLBO.js +82 -0
  7. package/dist/chunk-2FDSKLBO.js.map +1 -0
  8. package/dist/{chunk-RT6OILOI.js → chunk-4D2DOGAW.js} +2 -14
  9. package/dist/chunk-4D2DOGAW.js.map +1 -0
  10. package/dist/{chunk-OECZPRPW.js → chunk-64I6MYBC.js} +162 -775
  11. package/dist/chunk-64I6MYBC.js.map +1 -0
  12. package/dist/chunk-7473HK24.js +17 -0
  13. package/dist/chunk-7473HK24.js.map +1 -0
  14. package/dist/chunk-7Y5ROPXL.js +33 -0
  15. package/dist/chunk-7Y5ROPXL.js.map +1 -0
  16. package/dist/chunk-A2RBQD66.js +7 -0
  17. package/dist/chunk-A2RBQD66.js.map +1 -0
  18. package/dist/chunk-AK23XSWD.js +16 -0
  19. package/dist/chunk-AK23XSWD.js.map +1 -0
  20. package/dist/chunk-ASAMUGHG.js +651 -0
  21. package/dist/chunk-ASAMUGHG.js.map +1 -0
  22. package/dist/{chunk-B2P7AZ25.js → chunk-CYE4UBOB.js} +6 -4
  23. package/dist/chunk-CYE4UBOB.js.map +1 -0
  24. package/dist/chunk-DKE6QO26.js +136 -0
  25. package/dist/chunk-DKE6QO26.js.map +1 -0
  26. package/dist/chunk-DNP65TD5.js +832 -0
  27. package/dist/chunk-DNP65TD5.js.map +1 -0
  28. package/dist/{chunk-H23KEFBA.js → chunk-DUBU3BB3.js} +30 -1
  29. package/dist/chunk-DUBU3BB3.js.map +1 -0
  30. package/dist/chunk-GYLWEJHW.js +19 -0
  31. package/dist/chunk-GYLWEJHW.js.map +1 -0
  32. package/dist/{chunk-SHEGLZEP.js → chunk-I3JCFS7H.js} +34 -13
  33. package/dist/chunk-I3JCFS7H.js.map +1 -0
  34. package/dist/{chunk-X5CWPX7H.js → chunk-IIQ3WMJK.js} +13 -2
  35. package/dist/chunk-IIQ3WMJK.js.map +1 -0
  36. package/dist/{chunk-RZTP4IEW.js → chunk-JVTONQFI.js} +6339 -4226
  37. package/dist/chunk-JVTONQFI.js.map +1 -0
  38. package/dist/{chunk-QEKVYG5F.js → chunk-KKQANRLG.js} +2 -2
  39. package/dist/chunk-KKQANRLG.js.map +1 -0
  40. package/dist/{chunk-CDKG4TFA.js → chunk-MPQAT55F.js} +38 -205
  41. package/dist/chunk-MPQAT55F.js.map +1 -0
  42. package/dist/chunk-NO4M6PO5.js +101 -0
  43. package/dist/chunk-NO4M6PO5.js.map +1 -0
  44. package/dist/{chunk-TBMKJ26R.js → chunk-NW6U45HX.js} +3 -1
  45. package/dist/chunk-NW6U45HX.js.map +1 -0
  46. package/dist/chunk-NX2V6SIL.js +202 -0
  47. package/dist/chunk-NX2V6SIL.js.map +1 -0
  48. package/dist/{chunk-ZIVPHYRG.js → chunk-NZHA4TZD.js} +72 -175
  49. package/dist/chunk-NZHA4TZD.js.map +1 -0
  50. package/dist/{chunk-2PWMUVC7.js → chunk-OCZJ4EYV.js} +518 -27
  51. package/dist/chunk-OCZJ4EYV.js.map +1 -0
  52. package/dist/{chunk-REX5YPHX.js → chunk-PHCURHC7.js} +76 -21
  53. package/dist/chunk-PHCURHC7.js.map +1 -0
  54. package/dist/{chunk-4YO5H5ON.js → chunk-Q4OGQM5I.js} +4 -10
  55. package/dist/chunk-Q4OGQM5I.js.map +1 -0
  56. package/dist/chunk-QSU47XEX.js +13 -0
  57. package/dist/chunk-QSU47XEX.js.map +1 -0
  58. package/dist/{chunk-DD4SKCNO.js → chunk-RZXJTDH5.js} +13 -2
  59. package/dist/chunk-RZXJTDH5.js.map +1 -0
  60. package/dist/{chunk-Z2LZ7IZ7.js → chunk-SN3OZVEQ.js} +5 -31
  61. package/dist/chunk-SN3OZVEQ.js.map +1 -0
  62. package/dist/{chunk-4DIQHASK.js → chunk-SQ5VUG77.js} +28 -8
  63. package/dist/chunk-SQ5VUG77.js.map +1 -0
  64. package/dist/chunk-STH5DLQP.js +1192 -0
  65. package/dist/chunk-STH5DLQP.js.map +1 -0
  66. package/dist/{chunk-QCHKWIRS.js → chunk-VZFJTKIW.js} +13 -13
  67. package/dist/{chunk-QCHKWIRS.js.map → chunk-VZFJTKIW.js.map} +1 -1
  68. package/dist/{chunk-VSQBHPWH.js → chunk-X4ZU4QH5.js} +54 -22
  69. package/dist/chunk-X4ZU4QH5.js.map +1 -0
  70. package/dist/chunk-XD7SRDUY.js +188 -0
  71. package/dist/chunk-XD7SRDUY.js.map +1 -0
  72. package/dist/chunk-XH2IZVLT.js +62 -0
  73. package/dist/chunk-XH2IZVLT.js.map +1 -0
  74. package/dist/chunk-Z7I6SONO.js +825 -0
  75. package/dist/chunk-Z7I6SONO.js.map +1 -0
  76. package/dist/cli/quickdraw-docs.js +132 -112
  77. package/dist/cli/quickdraw-docs.js.map +1 -1
  78. package/dist/client/core.d.ts +387 -0
  79. package/dist/client/core.js +109 -0
  80. package/dist/client/index.d.ts +52 -203
  81. package/dist/client/index.js +59 -218
  82. package/dist/client/index.js.map +1 -1
  83. package/dist/{clientTypes-Bc-G8n-U.d.ts → clientTypes-CUUNJwWs.d.ts} +3 -3
  84. package/dist/{connection-Cr1BU07W.d.ts → connection-2EeQ_Et0.d.ts} +22 -11
  85. package/dist/{createServer-CP9eDHZZ.d.ts → createServer-Bx5HOcYK.d.ts} +67 -17
  86. package/dist/{createTestApp-UKVGEmOf.d.ts → createTestApp-DAMANT79.d.ts} +46 -13
  87. package/dist/{envelope-Cp3YcVxC.d.ts → envelope-BMBaxppV.d.ts} +216 -10
  88. package/dist/{http-Djj2Inuu.d.ts → http-YzLAar3w.d.ts} +76 -60
  89. package/dist/index.d.ts +8 -7
  90. package/dist/index.js +82 -25
  91. package/dist/index.js.map +1 -1
  92. package/dist/{infer-Teo1xpsd.d.ts → infer-Br62cnyW.d.ts} +82 -11
  93. package/dist/logger-Yz93KzdV.d.ts +30 -0
  94. package/dist/{metrics-BdBu5o8G.d.ts → metrics-CTVpw0gu.d.ts} +2 -2
  95. package/dist/origins-kvpxnVcW.d.ts +8 -0
  96. package/dist/parser.js +2 -1
  97. package/dist/prisma/index.d.ts +14 -7
  98. package/dist/prisma/index.js +279 -70
  99. package/dist/prisma/index.js.map +1 -1
  100. package/dist/{rateLimit-ZMIZJUZF.js → rateLimit-5RKBFLUR.js} +2 -2
  101. package/dist/rateLimit-5RKBFLUR.js.map +1 -0
  102. package/dist/remote-N7OUAGOW.js +15 -0
  103. package/dist/remote-N7OUAGOW.js.map +1 -0
  104. package/dist/server/auth/index.d.ts +82 -33
  105. package/dist/server/auth/index.js +277 -270
  106. package/dist/server/auth/index.js.map +1 -1
  107. package/dist/server/express/index.d.ts +9 -2
  108. package/dist/server/express/index.js +1 -1
  109. package/dist/server/index.d.ts +268 -84
  110. package/dist/server/index.js +664 -418
  111. package/dist/server/index.js.map +1 -1
  112. package/dist/server/mcp/index.d.ts +89 -27
  113. package/dist/server/mcp/index.js +281 -62
  114. package/dist/server/mcp/index.js.map +1 -1
  115. package/dist/server/otel.d.ts +3 -3
  116. package/dist/{sharing-DVa8W-yR.d.ts → sharing-BrsmlDv_.d.ts} +3 -3
  117. package/dist/testing/client.d.ts +14 -14
  118. package/dist/testing/client.js +19 -10
  119. package/dist/testing/client.js.map +1 -1
  120. package/dist/testing/index.d.ts +50 -13
  121. package/dist/testing/index.js +113 -21
  122. package/dist/testing/index.js.map +1 -1
  123. package/dist/testing/mock.d.ts +5 -5
  124. package/dist/testing/mock.js +17 -9
  125. package/dist/tracked-BR5b2O-U.d.ts +43 -0
  126. package/dist/{types-Cu0vmx7c.d.ts → types-CLRCQGnO.d.ts} +4 -3
  127. package/dist/types-tMagHbjy.d.ts +151 -0
  128. package/dist/utils/index.d.ts +16 -4
  129. package/dist/utils/index.js +6 -3
  130. package/package.json +5 -1
  131. package/dist/chunk-2PWMUVC7.js.map +0 -1
  132. package/dist/chunk-4DIQHASK.js.map +0 -1
  133. package/dist/chunk-4YO5H5ON.js.map +0 -1
  134. package/dist/chunk-B2P7AZ25.js.map +0 -1
  135. package/dist/chunk-CDKG4TFA.js.map +0 -1
  136. package/dist/chunk-DD4SKCNO.js.map +0 -1
  137. package/dist/chunk-H23KEFBA.js.map +0 -1
  138. package/dist/chunk-OECZPRPW.js.map +0 -1
  139. package/dist/chunk-QEKVYG5F.js.map +0 -1
  140. package/dist/chunk-REX5YPHX.js.map +0 -1
  141. package/dist/chunk-RT6OILOI.js.map +0 -1
  142. package/dist/chunk-RZTP4IEW.js.map +0 -1
  143. package/dist/chunk-SHEGLZEP.js.map +0 -1
  144. package/dist/chunk-TBMKJ26R.js.map +0 -1
  145. package/dist/chunk-VSQBHPWH.js.map +0 -1
  146. package/dist/chunk-X5CWPX7H.js.map +0 -1
  147. package/dist/chunk-YSWH776Y.js +0 -13
  148. package/dist/chunk-YSWH776Y.js.map +0 -1
  149. package/dist/chunk-Z2LZ7IZ7.js.map +0 -1
  150. package/dist/chunk-ZIVPHYRG.js.map +0 -1
  151. package/dist/logger-BY28acW-.d.ts +0 -16
  152. /package/dist/{rateLimit-ZMIZJUZF.js.map → client/core.js.map} +0 -0
@@ -3,34 +3,51 @@ import {
3
3
  } from "../../chunk-XNLWBZIG.js";
4
4
  import {
5
5
  NEVER_ABORTED,
6
+ callerGrantsOf,
7
+ checkKind,
8
+ inputKeys,
6
9
  lazyMembers,
10
+ narrowKinds,
7
11
  parseInput,
8
- toQuickdrawError
9
- } from "../../chunk-2PWMUVC7.js";
12
+ pointerToken,
13
+ referenced,
14
+ runtimeOf,
15
+ toQuickdrawError,
16
+ withCallerGrants
17
+ } from "../../chunk-OCZJ4EYV.js";
10
18
  import {
11
19
  describeError
12
- } from "../../chunk-DD4SKCNO.js";
20
+ } from "../../chunk-RZXJTDH5.js";
13
21
  import {
22
+ originAllowlist
23
+ } from "../../chunk-NO4M6PO5.js";
24
+ import {
25
+ isForbiddenCredential,
14
26
  isJsonRequest,
27
+ isPrincipal,
28
+ isRefusal,
15
29
  readJsonInput,
16
30
  toPrincipal,
17
31
  unreadable
18
- } from "../../chunk-REX5YPHX.js";
19
- import "../../chunk-H23KEFBA.js";
32
+ } from "../../chunk-PHCURHC7.js";
33
+ import "../../chunk-DUBU3BB3.js";
34
+ import "../../chunk-AK23XSWD.js";
35
+ import {
36
+ consoleLogger
37
+ } from "../../chunk-DKE6QO26.js";
20
38
  import {
21
39
  hasJsonSchema,
22
40
  isStandardSchema
23
41
  } from "../../chunk-47AEXVDM.js";
24
- import "../../chunk-Z2LZ7IZ7.js";
25
- import {
26
- consoleLogger
27
- } from "../../chunk-YSWH776Y.js";
42
+ import "../../chunk-SN3OZVEQ.js";
43
+ import "../../chunk-7Y5ROPXL.js";
28
44
  import {
29
45
  INTERNAL_MESSAGE,
30
46
  QuickdrawError,
31
47
  httpStatus,
32
48
  toWire
33
- } from "../../chunk-4YO5H5ON.js";
49
+ } from "../../chunk-Q4OGQM5I.js";
50
+ import "../../chunk-QSU47XEX.js";
34
51
 
35
52
  // src/server/mcp/tools.ts
36
53
  var JSON_SCHEMA_TARGET = "draft-07";
@@ -72,20 +89,72 @@ function toolInputOf(label, schema, fail, instead = `leave ${label} out with exc
72
89
  `the input schema of ${label} cannot be written as JSON Schema (${json.message}): change that schema, or ${instead}`
73
90
  );
74
91
  }
75
- if (json.type === "object") {
76
- return { schema: json, mode: "object" };
92
+ const described = referenced(json, json);
93
+ if (described?.type === "object") {
94
+ return { schema: inlined(json, described), mode: "object" };
77
95
  }
78
- const { $schema: dialect, ...input } = json;
96
+ const { $schema: dialect, definitions, $defs, ...input } = json;
79
97
  return {
80
98
  schema: {
81
99
  ...dialect === void 0 ? {} : { $schema: dialect },
82
100
  type: "object",
83
- properties: { input },
84
- ...acceptsUndefined(schema) ? {} : { required: ["input"] }
101
+ properties: { input: rerooted(input) },
102
+ ...acceptsUndefined(schema) ? {} : { required: ["input"] },
103
+ ...definitions === void 0 ? {} : { definitions: rerooted(definitions) },
104
+ ...$defs === void 0 ? {} : { $defs: rerooted($defs) }
85
105
  },
86
106
  mode: "wrapped"
87
107
  };
88
108
  }
109
+ var WRAPPED_INPUT = "#/properties/input";
110
+ function rerooted(value) {
111
+ if (Array.isArray(value)) {
112
+ return value.map(rerooted);
113
+ }
114
+ if (!isRecord(value)) {
115
+ return value;
116
+ }
117
+ return Object.fromEntries(
118
+ Object.entries(value).map(([key, item]) => {
119
+ const intoRoot = key === "$ref" && typeof item === "string" && (item === "#" || item.startsWith("#/") && !/^#\/(?:definitions|\$defs)\//u.test(item));
120
+ return [key, intoRoot ? `${WRAPPED_INPUT}${item.slice(1)}` : rerooted(item)];
121
+ })
122
+ );
123
+ }
124
+ function refersTo(value, ref) {
125
+ if (Array.isArray(value)) {
126
+ return value.some((item) => refersTo(item, ref));
127
+ }
128
+ if (!isRecord(value)) {
129
+ return false;
130
+ }
131
+ return value.$ref === ref || Object.values(value).some((item) => refersTo(item, ref));
132
+ }
133
+ function inlined(json, target) {
134
+ if (target === json) {
135
+ return json;
136
+ }
137
+ const ref = String(json.$ref);
138
+ const { $schema: dialect } = json;
139
+ const schema = {
140
+ ...dialect === void 0 ? {} : { $schema: dialect },
141
+ ...target
142
+ };
143
+ for (const keyword of ["definitions", "$defs"]) {
144
+ const defs = json[keyword];
145
+ if (!isRecord(defs)) {
146
+ continue;
147
+ }
148
+ const others = Object.fromEntries(
149
+ Object.entries(defs).filter(([name]) => `#/${keyword}/${pointerToken(name)}` !== ref)
150
+ );
151
+ const kept = refersTo({ ...schema, [keyword]: others }, ref) ? defs : others;
152
+ if (Object.keys(kept).length > 0) {
153
+ schema[keyword] = kept;
154
+ }
155
+ }
156
+ return schema;
157
+ }
89
158
  function inputOf(mode, args) {
90
159
  if (mode === "object") {
91
160
  return args ?? {};
@@ -139,35 +208,19 @@ function describeMethod(service, method, kind, text) {
139
208
  function propertiesOf(schema) {
140
209
  return isRecord(schema) && isRecord(schema.properties) ? Object.keys(schema.properties) : [];
141
210
  }
142
- function branchProperties(schema, found = /* @__PURE__ */ new Set()) {
143
- if (!isRecord(schema)) {
144
- return found;
145
- }
146
- for (const keyword of ["anyOf", "oneOf", "allOf"]) {
147
- const branches = schema[keyword];
148
- for (const branch of Array.isArray(branches) ? branches : []) {
149
- for (const key of propertiesOf(branch)) {
150
- found.add(key);
151
- }
152
- branchProperties(branch, found);
153
- }
154
- }
155
- return found;
156
- }
157
- function boundArguments(label, input, bind, fail) {
158
- if (input.mode === "object") {
159
- const properties = propertiesOf(input.schema);
160
- return bind.filter((key) => properties.includes(key));
211
+ function boundArguments(label, input, schema, bind, fail) {
212
+ if (bind.length === 0) {
213
+ return [];
161
214
  }
162
- const wrapped = input.schema.properties?.input;
163
- const unbindable = branchProperties(wrapped, new Set(propertiesOf(wrapped)));
164
- const misplaced = bind.find((key) => unbindable.has(key));
215
+ const bindable = input.mode === "object" ? propertiesOf(input.schema) : [];
216
+ const keys = inputKeys(schema) ?? [];
217
+ const misplaced = bind.find((key) => keys.includes(key) && !bindable.includes(key));
165
218
  if (misplaced !== void 0) {
166
219
  fail(
167
- `bind.${misplaced} names a property of the input of ${label}, which is not an object: bind fills properties of object inputs only; leave ${label} out with exclude, or make its input an object`
220
+ `bind.${misplaced} names a property of the input of ${label}, which is not an object: bind fills properties of object inputs only (an object schema, or one with .meta({ id }); not a union, an intersection, or a schema made from an object, as .optional() or .refine() make); leave ${label} out with exclude, or make its input an object`
168
221
  );
169
222
  }
170
- return [];
223
+ return bind.filter((key) => bindable.includes(key));
171
224
  }
172
225
  function withoutArguments(schema, bound) {
173
226
  const kept = (key) => !bound.includes(key);
@@ -194,7 +247,7 @@ function planMethodTools(services, options, fail, bind = []) {
194
247
  }
195
248
  const label = `${service.name}.${method}`;
196
249
  const input = toolInputOf(label, def.input, fail);
197
- const bound = boundArguments(label, input, bind, fail);
250
+ const bound = boundArguments(label, input, def.input, bind, fail);
198
251
  const tool = {
199
252
  name: toolName(service.name, method),
200
253
  description: describeMethod(service.name, method, def.kind, def.describe),
@@ -228,6 +281,46 @@ function describeTools(services, options = {}) {
228
281
  return planned.map((entry2) => entry2.tool);
229
282
  }
230
283
 
284
+ // src/server/mcp/lanes.ts
285
+ import { createHash } from "crypto";
286
+ import { isIPv6 } from "net";
287
+ function groupsOf(address) {
288
+ const written = new URL(`http://[${address}]/`).hostname.slice(1, -1);
289
+ const [head = "", tail = ""] = written.split("::");
290
+ const parse2 = (part) => part === "" ? [] : part.split(":").map((group) => Number.parseInt(group, 16));
291
+ const before = parse2(head);
292
+ const after = parse2(tail);
293
+ return [...before, ...new Array(8 - before.length - after.length).fill(0), ...after];
294
+ }
295
+ function addressLane(ip) {
296
+ const address = ip.split("%", 1)[0] ?? ip;
297
+ if (!isIPv6(address)) {
298
+ return ip;
299
+ }
300
+ const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0, g = 0, h = 0] = groupsOf(address);
301
+ if (a === 0 && b === 0 && c === 0 && d === 0 && e === 0 && f === 65535) {
302
+ return [g >> 8, g & 255, h >> 8, h & 255].join(".");
303
+ }
304
+ return `${[a, b, c, d & 65280].map((group) => group.toString(16)).join(":")}::/56`;
305
+ }
306
+ function addressOf(request) {
307
+ const { ip } = request.req;
308
+ return typeof ip === "string" && ip !== "" ? ip : request.req.socket?.remoteAddress ?? "unknown";
309
+ }
310
+ function laneOf(request, principal) {
311
+ if (request.transport === "stdio") {
312
+ return `mcp:${request.sessionId}`;
313
+ }
314
+ if (principal === null) {
315
+ return `mcp:http:address:${addressLane(addressOf(request))}`;
316
+ }
317
+ if (request.token === null) {
318
+ return `mcp:http:user:${principal.userId}`;
319
+ }
320
+ const hash = createHash("sha256").update(request.token).digest("base64url").slice(0, 22);
321
+ return `mcp:http:token:${hash}`;
322
+ }
323
+
231
324
  // src/server/mcp/session.ts
232
325
  var CATEGORY = "quickdraw.mcp";
233
326
  async function principalOf(settings, request) {
@@ -237,7 +330,8 @@ async function principalOf(settings, request) {
237
330
  try {
238
331
  return toPrincipal(await settings.principal(request));
239
332
  } catch (error) {
240
- settings.logger.error("MCP authentication failed", {
333
+ const refused = isRefusal(error) || isForbiddenCredential(error);
334
+ settings.logger[refused ? "debug" : "error"]("MCP authentication failed", {
241
335
  category: CATEGORY,
242
336
  transport: request.transport,
243
337
  error: describeError(error)
@@ -258,10 +352,22 @@ async function contextOf(settings, request, principal) {
258
352
  }
259
353
  return fields;
260
354
  }
261
- async function openSession(settings, request) {
262
- const principal = await principalOf(settings, request);
355
+ function givenPrincipal(principal) {
356
+ if (principal === null || principal === void 0) {
357
+ return null;
358
+ }
359
+ if (!isPrincipal(principal)) {
360
+ throw new TypeError(
361
+ "An MCP call's principal must be a principal with a non-empty string userId, or null"
362
+ );
363
+ }
364
+ return principal;
365
+ }
366
+ async function openSession(settings, options) {
367
+ const { request } = options;
368
+ const principal = Object.hasOwn(options, "principal") ? await withCallerGrants(givenPrincipal(options.principal), callerGrantsOf(settings.dispatcher)) : await principalOf(settings, request);
263
369
  const mcp = await contextOf(settings, request, principal);
264
- return { principal, mcp, connectionId: `mcp:${request.sessionId}` };
370
+ return { principal, mcp, connectionId: laneOf(request, principal) };
265
371
  }
266
372
  function failure(settings, error, tool) {
267
373
  const converted = toQuickdrawError(error);
@@ -342,7 +448,26 @@ function accessOf(owner, access, fail) {
342
448
  }
343
449
  return access;
344
450
  }
345
- function checkCustomTool(value, index, fail) {
451
+ function appKindsOf(services) {
452
+ let kinds;
453
+ for (const service of services) {
454
+ const own = runtimeOf(service)?.kinds;
455
+ if (own !== void 0) {
456
+ kinds = kinds === void 0 ? own : kinds.filter((kind) => own.includes(kind));
457
+ }
458
+ }
459
+ return kinds;
460
+ }
461
+ function kindsOf(owner, value, access, appKinds, fail) {
462
+ if (value.kinds !== void 0 && access === "public") {
463
+ fail(
464
+ `${owner}: a "public" tool takes no kinds of its own, since a caller of a refused kind could call it signed out; give it access "authenticated"`
465
+ );
466
+ }
467
+ const above = appKinds === void 0 ? void 0 : { kinds: appKinds, by: "initQuickdraw" };
468
+ return narrowKinds(value.kinds, above, (message) => fail(`${owner}: ${message}`));
469
+ }
470
+ function checkCustomTool(value, index, appKinds, fail) {
346
471
  if (!isRecord2(value)) {
347
472
  fail(`customTools[${index}] must be { name, description, inputSchema, handler }`);
348
473
  }
@@ -361,6 +486,7 @@ function checkCustomTool(value, index, fail) {
361
486
  fail(`${owner}: annotations must be an object of hints`);
362
487
  }
363
488
  const access = accessOf(owner, value.access, fail);
489
+ const kinds = kindsOf(owner, value, access, appKinds, fail);
364
490
  const input = argumentsOf(owner, inputSchema, fail);
365
491
  const tool = {
366
492
  name,
@@ -371,19 +497,26 @@ function checkCustomTool(value, index, fail) {
371
497
  return {
372
498
  tool: Object.freeze(tool),
373
499
  access,
500
+ kinds,
374
501
  mode: input.mode,
375
502
  schema: isStandardSchema(inputSchema) ? inputSchema : void 0,
376
503
  handler
377
504
  };
378
505
  }
379
- function checkCustomTools(value, fail) {
506
+ function checkCustomTools(value, fail, services = []) {
380
507
  if (value === void 0) {
381
508
  return [];
382
509
  }
383
510
  if (!Array.isArray(value)) {
384
511
  fail("customTools must be an array of { name, description, inputSchema, handler }");
385
512
  }
386
- return value.map((tool, index) => checkCustomTool(tool, index, fail));
513
+ const appKinds = appKindsOf(services);
514
+ if (appKinds?.length === 0 && value.length > 0) {
515
+ fail(
516
+ "customTools: the served services' apps admit no kind in common (initQuickdraw({ kinds })), so no principal could call a custom tool: serve custom tools beside the services of one app, or of apps that admit a kind in common"
517
+ );
518
+ }
519
+ return value.map((tool, index) => checkCustomTool(tool, index, appKinds, fail));
387
520
  }
388
521
  async function runCustomTool(settings, custom, args, session, options) {
389
522
  const { name } = custom.tool;
@@ -392,6 +525,7 @@ async function runCustomTool(settings, custom, args, session, options) {
392
525
  if (custom.access === "authenticated" && session.principal === null) {
393
526
  throw new QuickdrawError("UNAUTHENTICATED", "Authentication required");
394
527
  }
528
+ checkKind(custom.kinds, session.principal, `The tool "${name}"`);
395
529
  const input = inputOf(custom.mode, args);
396
530
  const parsed = custom.schema === void 0 ? input : await parseInput(custom.schema, input, `the tool "${name}"`);
397
531
  const data = await custom.handler({
@@ -475,7 +609,7 @@ function resolveRegistry(options) {
475
609
  const binders = checkBinders(options.bind, fail);
476
610
  const methodTools = planMethodTools(options.services, options, fail, [...binders.keys()]);
477
611
  checkBound(binders, methodTools, fail);
478
- const customTools = checkCustomTools(options.customTools, fail);
612
+ const customTools = checkCustomTools(options.customTools, fail, options.services);
479
613
  const entries = [
480
614
  ...methodTools.map((tool) => ({ kind: "method", ...tool })),
481
615
  ...customTools.map((tool) => ({ kind: "custom", ...tool }))
@@ -500,7 +634,7 @@ function resolveRegistry(options) {
500
634
  async function inSession(settings, name, options, run) {
501
635
  let session;
502
636
  try {
503
- session = await openSession(settings, options.request);
637
+ session = await openSession(settings, options);
504
638
  } catch (error) {
505
639
  return settle(settings, options, failure(settings, error, name));
506
640
  }
@@ -545,6 +679,16 @@ async function bindInput(settings, tool, input, session, request) {
545
679
  return { ...given, ...Object.fromEntries(filled) };
546
680
  }
547
681
  async function dispatchMethod(settings, tool, input, session, options) {
682
+ const stray = settings.binders.size === 0 || !isRecord3(input) || Array.isArray(input) ? void 0 : [...settings.binders.keys()].find(
683
+ (key) => !tool.bound.includes(key) && Object.hasOwn(input, key)
684
+ );
685
+ if (stray !== void 0) {
686
+ const error = new QuickdrawError(
687
+ "FORBIDDEN",
688
+ `"${stray}" is bound to the caller; ${tool.service}.${tool.method} does not take it from the agent`
689
+ );
690
+ return settle(settings, options, failure(settings, error, tool.tool.name));
691
+ }
548
692
  let bound = input;
549
693
  if (tool.bound.length > 0) {
550
694
  try {
@@ -894,8 +1038,81 @@ async function bootstrapMcpServer(serverModule) {
894
1038
  }
895
1039
  }
896
1040
 
897
- // src/server/mcp/http.ts
1041
+ // src/server/mcp/httpRequest.ts
898
1042
  import { randomUUID as randomUUID2 } from "crypto";
1043
+ function requestOf(req) {
1044
+ const header = req.headers.authorization;
1045
+ const token = typeof header === "string" && header.startsWith("Bearer ") ? header.slice(7) : "";
1046
+ return {
1047
+ transport: "http",
1048
+ sessionId: randomUUID2(),
1049
+ token: token.trim() === "" ? null : token.trim(),
1050
+ headers: req.headers,
1051
+ req
1052
+ };
1053
+ }
1054
+ function hostOf(value) {
1055
+ if (/\s/u.test(value)) {
1056
+ return void 0;
1057
+ }
1058
+ const host = !value.startsWith("[") && value.split(":").length > 2 ? `[${value}]` : value;
1059
+ let url;
1060
+ try {
1061
+ url = new URL(`http://${host}`);
1062
+ } catch {
1063
+ return void 0;
1064
+ }
1065
+ const extra = url.username + url.password + url.search + url.hash;
1066
+ if (url.hostname === "" || extra !== "" || url.pathname !== "/") {
1067
+ return void 0;
1068
+ }
1069
+ const written = !host.endsWith("]") && /:\d+$/u.test(host);
1070
+ return { hostname: url.hostname, port: written ? url.port || "80" : void 0 };
1071
+ }
1072
+ function checkHosts(hosts) {
1073
+ if (hosts === void 0) {
1074
+ return void 0;
1075
+ }
1076
+ const parsed = Array.isArray(hosts) ? hosts.map((host) => typeof host === "string" ? hostOf(host) : void 0) : [];
1077
+ if (parsed.length === 0 || parsed.includes(void 0)) {
1078
+ throw new TypeError(
1079
+ 'createMcpHttpRouter: allowedHosts must list host names, such as "localhost" or "mcp.example.com:8443"'
1080
+ );
1081
+ }
1082
+ return Object.freeze(parsed);
1083
+ }
1084
+ function hostAllowed(hosts, header) {
1085
+ const host = header === void 0 ? void 0 : hostOf(header);
1086
+ if (host === void 0) {
1087
+ return false;
1088
+ }
1089
+ const portMatches = (port) => host.port === void 0 ? port === "80" || port === "443" : port === host.port;
1090
+ return hosts.some(
1091
+ (entry2) => entry2.hostname === host.hostname && (entry2.port === void 0 || portMatches(entry2.port))
1092
+ );
1093
+ }
1094
+ function requestGate(allowedOrigins, allowedHosts) {
1095
+ const origins = originAllowlist(
1096
+ allowedOrigins ?? [],
1097
+ "createMcpHttpRouter",
1098
+ true
1099
+ );
1100
+ const hosts = checkHosts(allowedHosts);
1101
+ return Object.freeze({
1102
+ refusal(req) {
1103
+ const { origin, host } = req.headers;
1104
+ if (origin !== void 0 && origins.allowed(origin) === null) {
1105
+ return "The MCP routes refuse requests from web pages unless allowedOrigins lists their origin";
1106
+ }
1107
+ if (hosts !== void 0 && !hostAllowed(hosts, host)) {
1108
+ return "The MCP routes do not answer for this host";
1109
+ }
1110
+ return void 0;
1111
+ }
1112
+ });
1113
+ }
1114
+
1115
+ // src/server/mcp/http.ts
899
1116
  var DEFAULT_MAX_BODY_BYTES = 1048576;
900
1117
  var CATEGORY3 = "quickdraw.mcp";
901
1118
  var NOT_JSON = "Send the call as a JSON body with Content-Type: application/json";
@@ -922,6 +1139,8 @@ function send(res, status, body) {
922
1139
  res.statusCode = status;
923
1140
  res.setHeader("content-type", "application/json; charset=utf-8");
924
1141
  res.setHeader("content-length", bytes);
1142
+ res.setHeader("cache-control", "no-store");
1143
+ res.setHeader("x-content-type-options", "nosniff");
925
1144
  res.end(body);
926
1145
  return bytes;
927
1146
  }
@@ -962,17 +1181,6 @@ function targetOf(body) {
962
1181
  }
963
1182
  return void 0;
964
1183
  }
965
- function requestOf(req) {
966
- const header = req.headers.authorization;
967
- const token = typeof header === "string" && header.startsWith("Bearer ") ? header.slice(7) : "";
968
- return {
969
- transport: "http",
970
- sessionId: randomUUID2(),
971
- token: token.trim() === "" ? null : token.trim(),
972
- headers: req.headers,
973
- req
974
- };
975
- }
976
1184
  async function invoke(settings, req, res) {
977
1185
  const controller = new AbortController();
978
1186
  res.once("close", () => {
@@ -1022,7 +1230,8 @@ function resolveSettings(options) {
1022
1230
  registry: options.registry,
1023
1231
  prefix: normalizePath(options.path ?? "/mcp"),
1024
1232
  maxBodyBytes,
1025
- logger: options.logger ?? consoleLogger
1233
+ logger: options.logger ?? consoleLogger,
1234
+ gate: requestGate(options.allowedOrigins, options.allowedHosts)
1026
1235
  };
1027
1236
  }
1028
1237
  function createMcpHttpRouter(options) {
@@ -1037,6 +1246,16 @@ function createMcpHttpRouter(options) {
1037
1246
  }
1038
1247
  return;
1039
1248
  }
1249
+ const refusal = settings.gate.refusal(req);
1250
+ if (refusal !== void 0) {
1251
+ settings.logger.debug("An MCP HTTP request was refused", {
1252
+ category: CATEGORY3,
1253
+ origin: req.headers.origin,
1254
+ host: req.headers.host
1255
+ });
1256
+ sendResult(settings, res, failure2(new QuickdrawError("FORBIDDEN", refusal)));
1257
+ return;
1258
+ }
1040
1259
  if (route === "tools") {
1041
1260
  send(res, 200, JSON.stringify({ tools: settings.registry.tools }));
1042
1261
  return;