@fitzzero/quickdraw-core 5.0.1 → 5.1.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 (60) hide show
  1. package/README.md +303 -32
  2. package/dist/{admin-CBlTY0do.d.ts → admin-DKwkIh5i.d.ts} +2 -2
  3. package/dist/{adminSchemas-zji11V74.d.ts → adminSchemas-2DUA_hZ8.d.ts} +1 -1
  4. package/dist/{auth-CMyRkfjZ.d.ts → auth-BhWqdyb-.d.ts} +2 -2
  5. package/dist/{caller-BqCYnpmC.d.ts → caller-CXTFswBO.d.ts} +27 -5
  6. package/dist/{chunk-2XJFNVHW.js → chunk-2PWMUVC7.js} +1 -1
  7. package/dist/{chunk-2XJFNVHW.js.map → chunk-2PWMUVC7.js.map} +1 -1
  8. package/dist/{chunk-3JNMVB2V.js → chunk-2UELNHDD.js} +2 -2
  9. package/dist/{chunk-HQ6WIRE2.js → chunk-3674EIRT.js} +4 -4
  10. package/dist/{chunk-NS5PUHIZ.js → chunk-A6DJBGOG.js} +130 -23
  11. package/dist/chunk-A6DJBGOG.js.map +1 -0
  12. package/dist/{chunk-ZACRRYWL.js → chunk-JZBCLNDU.js} +4 -4
  13. package/dist/{chunk-O2PWC2YW.js → chunk-KNDDCVQJ.js} +3 -3
  14. package/dist/{chunk-5YRQ26LF.js → chunk-RT6OILOI.js} +1 -1
  15. package/dist/chunk-RT6OILOI.js.map +1 -0
  16. package/dist/{chunk-WVIRXYI4.js → chunk-VJTFXHOK.js} +2 -2
  17. package/dist/{chunk-WVIRXYI4.js.map → chunk-VJTFXHOK.js.map} +1 -1
  18. package/dist/chunk-XNLWBZIG.js +9 -0
  19. package/dist/chunk-XNLWBZIG.js.map +1 -0
  20. package/dist/cli/quickdraw-docs.js +241 -27
  21. package/dist/cli/quickdraw-docs.js.map +1 -1
  22. package/dist/client/index.d.ts +7 -7
  23. package/dist/client/index.js +4 -4
  24. package/dist/{clientTypes-DuZW_8mb.d.ts → clientTypes-Bc-G8n-U.d.ts} +3 -3
  25. package/dist/{connection-DuN8GvcD.d.ts → connection-Cr1BU07W.d.ts} +2 -2
  26. package/dist/{createServer-CGsJlxM8.d.ts → createServer-CP9eDHZZ.d.ts} +30 -9
  27. package/dist/{createTestApp-Cg2CZa1g.d.ts → createTestApp-UKVGEmOf.d.ts} +6 -6
  28. package/dist/{envelope-nf5eGB-a.d.ts → envelope-Cp3YcVxC.d.ts} +1 -1
  29. package/dist/{http-D3fSFQ57.d.ts → http-Djj2Inuu.d.ts} +6 -6
  30. package/dist/index.d.ts +6 -6
  31. package/dist/index.js +26 -6
  32. package/dist/index.js.map +1 -1
  33. package/dist/{infer-Dw21kmLw.d.ts → infer-Teo1xpsd.d.ts} +17 -0
  34. package/dist/{metrics-C0WvB7-I.d.ts → metrics-BdBu5o8G.d.ts} +2 -2
  35. package/dist/prisma/index.d.ts +4 -4
  36. package/dist/server/auth/index.d.ts +3 -3
  37. package/dist/server/index.d.ts +47 -26
  38. package/dist/server/index.js +62 -36
  39. package/dist/server/index.js.map +1 -1
  40. package/dist/server/mcp/index.d.ts +51 -11
  41. package/dist/server/mcp/index.js +129 -8
  42. package/dist/server/mcp/index.js.map +1 -1
  43. package/dist/server/otel.d.ts +3 -3
  44. package/dist/{sharing-CoreezAq.d.ts → sharing-DVa8W-yR.d.ts} +3 -3
  45. package/dist/testing/client.d.ts +13 -13
  46. package/dist/testing/client.js +5 -5
  47. package/dist/testing/index.d.ts +156 -12
  48. package/dist/testing/index.js +1036 -21
  49. package/dist/testing/index.js.map +1 -1
  50. package/dist/testing/mock.d.ts +5 -5
  51. package/dist/testing/mock.js +4 -4
  52. package/dist/{types-Bwg3kIEc.d.ts → types-Cu0vmx7c.d.ts} +8 -2
  53. package/dist/utils/index.d.ts +1 -1
  54. package/package.json +1 -1
  55. package/dist/chunk-5YRQ26LF.js.map +0 -1
  56. package/dist/chunk-NS5PUHIZ.js.map +0 -1
  57. /package/dist/{chunk-3JNMVB2V.js.map → chunk-2UELNHDD.js.map} +0 -0
  58. /package/dist/{chunk-HQ6WIRE2.js.map → chunk-3674EIRT.js.map} +0 -0
  59. /package/dist/{chunk-ZACRRYWL.js.map → chunk-JZBCLNDU.js.map} +0 -0
  60. /package/dist/{chunk-O2PWC2YW.js.map → chunk-KNDDCVQJ.js.map} +0 -0
@@ -1,14 +1,14 @@
1
- import { A as AnyService, i as Caller } from '../../caller-BqCYnpmC.js';
1
+ import { A as AnyService, d as Caller } from '../../caller-CXTFswBO.js';
2
2
  import { IncomingHttpHeaders, IncomingMessage } from 'node:http';
3
- import { ap as MethodName, d as StandardSchemaV1, c as InferOutput } from '../../infer-Dw21kmLw.js';
3
+ import { ap as MethodName, d as StandardSchemaV1, c as InferOutput } from '../../infer-Teo1xpsd.js';
4
4
  import { Q as QuickdrawError, a as ErrorCode } from '../../errors-S-bxEIBC.js';
5
- import { P as PrincipalOfServices, C as ContractOfServices, D as Dispatcher, H as HttpRouter } from '../../http-D3fSFQ57.js';
6
- import { a as McpContext, b as McpContextOf, M as MaybePromise } from '../../types-Bwg3kIEc.js';
5
+ import { P as PrincipalOfServices, C as ContractOfServices, D as Dispatcher, H as HttpRouter } from '../../http-Djj2Inuu.js';
6
+ import { a as McpContext, b as McpContextOf, M as MaybePromise } from '../../types-Cu0vmx7c.js';
7
7
  import { L as Logger } from '../../logger-BY28acW-.js';
8
- import { A as AuthenticateResult } from '../../auth-CMyRkfjZ.js';
8
+ import { A as AuthenticateResult } from '../../auth-BhWqdyb-.js';
9
9
  import { Readable, Writable } from 'node:stream';
10
- import '../../envelope-nf5eGB-a.js';
11
- import '../../metrics-C0WvB7-I.js';
10
+ import '../../envelope-Cp3YcVxC.js';
11
+ import '../../metrics-BdBu5o8G.js';
12
12
  import 'socket.io';
13
13
 
14
14
  /** The JSON Schema of a tool's arguments. MCP requires an object schema. */
@@ -134,6 +134,28 @@ interface McpToolCall<S extends readonly AnyService[] = readonly AnyService[], A
134
134
  readonly signal: AbortSignal;
135
135
  readonly request: McpRequest;
136
136
  }
137
+ /** What a function of the registry's `bind` receives: one call of a tool whose method's input has its argument. */
138
+ interface McpBindCall<S extends readonly AnyService[] = readonly AnyService[]> {
139
+ /**
140
+ * Who the call acts for, from the registry's `principal`. Never `null`: an
141
+ * anonymous caller of a tool with a bound argument is refused with
142
+ * `UNAUTHENTICATED` before any binder runs.
143
+ */
144
+ readonly principal: PrincipalOfServices<S>;
145
+ /** The fields the registry's `context` produced for the call, if any. */
146
+ readonly mcp: McpContextOfServices<S> | undefined;
147
+ readonly request: McpRequest;
148
+ /** The service and method the tool calls, for example `"messageService"` and `"post"`. */
149
+ readonly service: string;
150
+ readonly method: string;
151
+ }
152
+ /**
153
+ * Fills one argument of the registry's `bind` from who is calling, for
154
+ * example a claim of the principal's token. Returning `undefined` or `null`
155
+ * refuses the call with `FORBIDDEN`; throwing fails it with `INTERNAL`, or
156
+ * with the code of a `QuickdrawError`.
157
+ */
158
+ type McpBinder<S extends readonly AnyService[] = readonly AnyService[]> = (call: McpBindCall<S>) => MaybePromise<unknown>;
137
159
  /**
138
160
  * Who may call a custom tool: `"authenticated"` callers only, or `"public"`,
139
161
  * anonymous callers too. These are the two method access forms that need no
@@ -206,6 +228,21 @@ interface McpRegistryOptions<S extends readonly AnyService[], Tools extends read
206
228
  * for example. Runs per call, after `principal`.
207
229
  */
208
230
  readonly context?: (request: McpRequest, principal: PrincipalOfServices<S> | null) => MaybePromise<McpContextOfServices<S>>;
231
+ /**
232
+ * Arguments filled from who is calling instead of by the agent, by name:
233
+ * `{ taskId: ({ principal }) => principal.claims?.taskId }`. Each one is
234
+ * left out of the `inputSchema` of every method's tool whose object input
235
+ * has it, and each call of such a tool, by `call` or `callMethod`, gets the
236
+ * binder's value before its input is validated. The call is refused
237
+ * before the method runs when the caller is anonymous (`UNAUTHENTICATED`),
238
+ * or when the binder returns `undefined` or `null` or the caller sent the
239
+ * argument with another value (`FORBIDDEN`); a binder that throws fails it
240
+ * with `INTERNAL`, or with the code of a `QuickdrawError`. Custom tools are
241
+ * never bound. Binding is not access control: the same principal reaches
242
+ * the method over a socket or HTTP with nothing bound, so the method's
243
+ * access must still confine it.
244
+ */
245
+ readonly bind?: Readonly<Record<string, McpBinder<S>>>;
209
246
  /** The app's own tools, listed after the generated ones. */
210
247
  readonly customTools?: {
211
248
  readonly [K in keyof Tools]: McpCustomTool<S, Tools[K]>;
@@ -235,7 +272,8 @@ interface McpRegistry {
235
272
  /**
236
273
  * Calls a method's tool by its service and method name with the method's
237
274
  * `input` as it is, without the tool's argument mapping: the shape 4.1's
238
- * HTTP route took. A method that is not a tool is `NOT_FOUND`.
275
+ * HTTP route took. Bound arguments are filled as for `call`. A method that
276
+ * is not a tool is `NOT_FOUND`.
239
277
  */
240
278
  callMethod(service: string, method: string, input: unknown, options: McpCallOptions): Promise<McpCallResult>;
241
279
  }
@@ -243,8 +281,10 @@ interface McpRegistry {
243
281
  * Builds the MCP tools of `services` (see `describeTools`) and the app's
244
282
  * `customTools`, served by `dispatcher`. Throws at once when a method's
245
283
  * input schema cannot describe itself as JSON Schema (use Zod 4.2 or later
246
- * for it, or `exclude` the method), when two tools share a name, or when the
247
- * dispatcher does not serve one of the services.
284
+ * for it, or `exclude` the method), when two tools share a name, when the
285
+ * dispatcher does not serve one of the services, or when an argument of
286
+ * `bind` is in no tool's object input or is in an input that is not an
287
+ * object.
248
288
  *
249
289
  * @example
250
290
  * const registry = createMcpRegistry({
@@ -366,4 +406,4 @@ type McpHttpReply = {
366
406
  */
367
407
  declare function createMcpHttpRouter(options: McpHttpRouterOptions): HttpRouter;
368
408
 
369
- export { type DescribeToolsOptions, MCP_PROTOCOL_VERSION, type McpCallOptions, type McpCallResult, type McpContextOfServices, type McpCustomTool, type McpHttpReply, type McpHttpRequest, type McpHttpRouterOptions, type McpInputSchema, type McpMethodRef, type McpRegistry, type McpRegistryOptions, type McpRequest, type McpStdioRequest, type McpStdioServer, type McpStdioServerOptions, type McpTextContent, type McpTool, type McpToolAccess, type McpToolAnnotations, type McpToolArguments, type McpToolCall, type McpToolInputSchema, type McpToolResult, bootstrapMcpServer, createMcpHttpRouter, createMcpRegistry, createMcpStdioServer, describeTools, toToolResult };
409
+ export { type DescribeToolsOptions, MCP_PROTOCOL_VERSION, type McpBindCall, type McpBinder, type McpCallOptions, type McpCallResult, type McpContextOfServices, type McpCustomTool, type McpHttpReply, type McpHttpRequest, type McpHttpRouterOptions, type McpInputSchema, type McpMethodRef, type McpRegistry, type McpRegistryOptions, type McpRequest, type McpStdioRequest, type McpStdioServer, type McpStdioServerOptions, type McpTextContent, type McpTool, type McpToolAccess, type McpToolAnnotations, type McpToolArguments, type McpToolCall, type McpToolInputSchema, type McpToolResult, bootstrapMcpServer, createMcpHttpRouter, createMcpRegistry, createMcpStdioServer, describeTools, toToolResult };
@@ -1,9 +1,12 @@
1
+ import {
2
+ defaultToolName
3
+ } from "../../chunk-XNLWBZIG.js";
1
4
  import {
2
5
  NEVER_ABORTED,
3
6
  lazyMembers,
4
7
  parseInput,
5
8
  toQuickdrawError
6
- } from "../../chunk-2XJFNVHW.js";
9
+ } from "../../chunk-2PWMUVC7.js";
7
10
  import {
8
11
  describeError
9
12
  } from "../../chunk-DD4SKCNO.js";
@@ -122,7 +125,7 @@ function toolNamer(options, fail) {
122
125
  fail("name must be a function of (service, method)");
123
126
  }
124
127
  return (service, method) => {
125
- const named = name === void 0 ? `${service}_${method}` : name(service, method);
128
+ const named = name === void 0 ? defaultToolName(service, method) : name(service, method);
126
129
  if (typeof named !== "string" || named === "") {
127
130
  const shown = typeof named === "string" ? `""` : String(named);
128
131
  fail(`name returned ${shown} for ${service}.${method}; a tool name is a non-empty string`);
@@ -133,7 +136,50 @@ function toolNamer(options, fail) {
133
136
  function describeMethod(service, method, kind, text) {
134
137
  return typeof text === "string" && text !== "" ? text : `${service}.${method} (${kind})`;
135
138
  }
136
- function planMethodTools(services, options, fail) {
139
+ function propertiesOf(schema) {
140
+ return isRecord(schema) && isRecord(schema.properties) ? Object.keys(schema.properties) : [];
141
+ }
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));
161
+ }
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));
165
+ if (misplaced !== void 0) {
166
+ 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`
168
+ );
169
+ }
170
+ return [];
171
+ }
172
+ function withoutArguments(schema, bound) {
173
+ const kept = (key) => !bound.includes(key);
174
+ const { properties = {}, required, ...rest } = schema;
175
+ const stillRequired = required?.filter(kept) ?? [];
176
+ return {
177
+ ...rest,
178
+ properties: Object.fromEntries(Object.entries(properties).filter(([key]) => kept(key))),
179
+ ...stillRequired.length === 0 ? {} : { required: stillRequired }
180
+ };
181
+ }
182
+ function planMethodTools(services, options, fail, bind = []) {
137
183
  const given = services;
138
184
  if (!Array.isArray(given)) {
139
185
  fail("services must be an array of services from qd.defineService");
@@ -148,13 +194,14 @@ function planMethodTools(services, options, fail) {
148
194
  }
149
195
  const label = `${service.name}.${method}`;
150
196
  const input = toolInputOf(label, def.input, fail);
197
+ const bound = boundArguments(label, input, bind, fail);
151
198
  const tool = {
152
199
  name: toolName(service.name, method),
153
200
  description: describeMethod(service.name, method, def.kind, def.describe),
154
- inputSchema: input.schema,
201
+ inputSchema: bound.length === 0 ? input.schema : withoutArguments(input.schema, bound),
155
202
  ...def.kind === "query" ? { annotations: { readOnlyHint: true } } : {}
156
203
  };
157
- planned.push({ ...input, tool: Object.freeze(tool), service: service.name, method });
204
+ planned.push({ ...input, tool: Object.freeze(tool), service: service.name, method, bound });
158
205
  }
159
206
  }
160
207
  return planned;
@@ -390,6 +437,31 @@ function checkDispatcher(dispatcher, services, fail) {
390
437
  }
391
438
  }
392
439
  }
440
+ function checkBinders(bind, fail) {
441
+ if (bind === void 0) {
442
+ return /* @__PURE__ */ new Map();
443
+ }
444
+ if (!isRecord3(bind) || Array.isArray(bind)) {
445
+ fail("bind must be an object of functions, one per argument it fills");
446
+ }
447
+ const binders = /* @__PURE__ */ new Map();
448
+ for (const [key, binder] of Object.entries(bind)) {
449
+ if (typeof binder !== "function") {
450
+ fail(`bind.${key} must be a function of the call, returning the argument's value`);
451
+ }
452
+ binders.set(key, binder);
453
+ }
454
+ return binders;
455
+ }
456
+ function checkBound(binders, methodTools, fail) {
457
+ for (const key of binders.keys()) {
458
+ if (!methodTools.some((tool) => tool.bound.includes(key))) {
459
+ fail(
460
+ `bind.${key} fills nothing: no method served as a tool has "${key}" in its object input`
461
+ );
462
+ }
463
+ }
464
+ }
393
465
  function resolveRegistry(options) {
394
466
  const fail = (message) => {
395
467
  throw new TypeError(`createMcpRegistry: ${message}`);
@@ -400,7 +472,9 @@ function resolveRegistry(options) {
400
472
  checkDispatcher(options.dispatcher, options.services, fail);
401
473
  checkHook("principal", options.principal, fail);
402
474
  checkHook("context", options.context, fail);
403
- const methodTools = planMethodTools(options.services, options, fail);
475
+ const binders = checkBinders(options.bind, fail);
476
+ const methodTools = planMethodTools(options.services, options, fail, [...binders.keys()]);
477
+ checkBound(binders, methodTools, fail);
404
478
  const customTools = checkCustomTools(options.customTools, fail);
405
479
  const entries = [
406
480
  ...methodTools.map((tool) => ({ kind: "method", ...tool })),
@@ -419,7 +493,8 @@ function resolveRegistry(options) {
419
493
  context: options.context,
420
494
  logger: options.logger ?? consoleLogger,
421
495
  entries: new Map(entries.map((entry2) => [entry2.tool.name, entry2])),
422
- methods: new Map(methodTools.map((tool) => [methodKey(tool.service, tool.method), tool]))
496
+ methods: new Map(methodTools.map((tool) => [methodKey(tool.service, tool.method), tool])),
497
+ binders
423
498
  };
424
499
  }
425
500
  async function inSession(settings, name, options, run) {
@@ -431,12 +506,58 @@ async function inSession(settings, name, options, run) {
431
506
  }
432
507
  return run(session);
433
508
  }
509
+ async function bindInput(settings, tool, input, session, request) {
510
+ const { principal } = session;
511
+ if (principal === null) {
512
+ throw new QuickdrawError("UNAUTHENTICATED", "Authentication required");
513
+ }
514
+ const given = input ?? {};
515
+ if (!isRecord3(given) || Array.isArray(given)) {
516
+ throw new QuickdrawError("VALIDATION", `Invalid input for ${tool.service}.${tool.method}`, {
517
+ issues: [{ path: [], message: "Expected an object" }]
518
+ });
519
+ }
520
+ const call = {
521
+ principal,
522
+ mcp: session.mcp,
523
+ request,
524
+ service: tool.service,
525
+ method: tool.method
526
+ };
527
+ const filled = [];
528
+ for (const [key, binder] of settings.binders) {
529
+ if (!tool.bound.includes(key)) {
530
+ continue;
531
+ }
532
+ const value = await binder(call);
533
+ if (value === void 0 || value === null) {
534
+ throw new QuickdrawError("FORBIDDEN", `"${key}" is bound to the caller, who has none`);
535
+ }
536
+ const sent = Object.hasOwn(given, key) ? given[key] : void 0;
537
+ if (sent !== void 0 && JSON.stringify(sent) !== JSON.stringify(value)) {
538
+ throw new QuickdrawError(
539
+ "FORBIDDEN",
540
+ `"${key}" is bound to the caller and cannot be set to another value`
541
+ );
542
+ }
543
+ filled.push([key, value]);
544
+ }
545
+ return { ...given, ...Object.fromEntries(filled) };
546
+ }
434
547
  async function dispatchMethod(settings, tool, input, session, options) {
548
+ let bound = input;
549
+ if (tool.bound.length > 0) {
550
+ try {
551
+ bound = await bindInput(settings, tool, input, session, options.request);
552
+ } catch (error) {
553
+ return settle(settings, options, failure(settings, error, tool.tool.name));
554
+ }
555
+ }
435
556
  const { respond } = options;
436
557
  const request = sessionRequest(session, {
437
558
  service: tool.service,
438
559
  method: tool.method,
439
- input,
560
+ input: bound,
440
561
  signal: options.signal,
441
562
  respond: respond === void 0 ? void 0 : (result) => respond(fromDispatch(result))
442
563
  });