@fitzzero/quickdraw-core 5.1.0 → 5.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +533 -180
- package/dist/{admin-DKwkIh5i.d.ts → admin-DWWrT1h6.d.ts} +2 -2
- package/dist/{adminSchemas-2DUA_hZ8.d.ts → adminSchemas-D4bFKsrb.d.ts} +6 -4
- package/dist/{auth-BhWqdyb-.d.ts → auth-DXiSJZaf.d.ts} +42 -16
- package/dist/{caller-CXTFswBO.d.ts → caller-BT-Mv4bD.d.ts} +117 -22
- package/dist/{chunk-RT6OILOI.js → chunk-4D2DOGAW.js} +2 -14
- package/dist/chunk-4D2DOGAW.js.map +1 -0
- package/dist/chunk-5CGO426C.js +15 -0
- package/dist/chunk-5CGO426C.js.map +1 -0
- package/dist/{chunk-A6DJBGOG.js → chunk-6K5BY47X.js} +4594 -3481
- package/dist/chunk-6K5BY47X.js.map +1 -0
- package/dist/{chunk-VJTFXHOK.js → chunk-753VJIUP.js} +13 -15
- package/dist/chunk-753VJIUP.js.map +1 -0
- package/dist/{chunk-JZBCLNDU.js → chunk-7QCUX4TK.js} +4 -4
- package/dist/chunk-7QCUX4TK.js.map +1 -0
- package/dist/chunk-AK23XSWD.js +16 -0
- package/dist/chunk-AK23XSWD.js.map +1 -0
- package/dist/{chunk-3674EIRT.js → chunk-AONAFOYU.js} +14 -5
- package/dist/{chunk-3674EIRT.js.map → chunk-AONAFOYU.js.map} +1 -1
- package/dist/chunk-DKE6QO26.js +136 -0
- package/dist/chunk-DKE6QO26.js.map +1 -0
- package/dist/{chunk-H23KEFBA.js → chunk-DUBU3BB3.js} +30 -1
- package/dist/chunk-DUBU3BB3.js.map +1 -0
- package/dist/{chunk-CDKG4TFA.js → chunk-GKH3LJ7N.js} +1 -1
- package/dist/chunk-GKH3LJ7N.js.map +1 -0
- package/dist/{chunk-KNDDCVQJ.js → chunk-GSIPUNJL.js} +49 -9
- package/dist/chunk-GSIPUNJL.js.map +1 -0
- package/dist/{chunk-DD4SKCNO.js → chunk-LCC7HNFP.js} +12 -1
- package/dist/chunk-LCC7HNFP.js.map +1 -0
- package/dist/{chunk-2PWMUVC7.js → chunk-MBH6IDSM.js} +480 -12
- package/dist/chunk-MBH6IDSM.js.map +1 -0
- package/dist/{chunk-2UELNHDD.js → chunk-NMXYENH4.js} +10 -10
- package/dist/{chunk-2UELNHDD.js.map → chunk-NMXYENH4.js.map} +1 -1
- package/dist/chunk-NO4M6PO5.js +101 -0
- package/dist/chunk-NO4M6PO5.js.map +1 -0
- package/dist/{chunk-TBMKJ26R.js → chunk-NW6U45HX.js} +3 -1
- package/dist/chunk-NW6U45HX.js.map +1 -0
- package/dist/{chunk-REX5YPHX.js → chunk-RJ5I2VO3.js} +74 -19
- package/dist/chunk-RJ5I2VO3.js.map +1 -0
- package/dist/{chunk-QEKVYG5F.js → chunk-RL4VKLBX.js} +1 -1
- package/dist/{chunk-QEKVYG5F.js.map → chunk-RL4VKLBX.js.map} +1 -1
- package/dist/chunk-XH2IZVLT.js +62 -0
- package/dist/chunk-XH2IZVLT.js.map +1 -0
- package/dist/{chunk-4DIQHASK.js → chunk-Z5ILULF2.js} +26 -7
- package/dist/chunk-Z5ILULF2.js.map +1 -0
- package/dist/cli/quickdraw-docs.js +5 -3
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/index.d.ts +30 -18
- package/dist/client/index.js +8 -6
- package/dist/client/index.js.map +1 -1
- package/dist/{clientTypes-Bc-G8n-U.d.ts → clientTypes-hWcPUVS9.d.ts} +3 -3
- package/dist/{connection-Cr1BU07W.d.ts → connection-CgijdGpT.d.ts} +8 -4
- package/dist/{createServer-CP9eDHZZ.d.ts → createServer-Cp-Wk500.d.ts} +36 -14
- package/dist/{createTestApp-UKVGEmOf.d.ts → createTestApp-BB_PkqQ7.d.ts} +16 -10
- package/dist/{envelope-Cp3YcVxC.d.ts → envelope-DzWyIXjj.d.ts} +18 -4
- package/dist/{http-Djj2Inuu.d.ts → http-DotQ5bj8.d.ts} +68 -17
- package/dist/index.d.ts +7 -7
- package/dist/index.js +37 -13
- package/dist/index.js.map +1 -1
- package/dist/{infer-Teo1xpsd.d.ts → infer-BmoZsL9a.d.ts} +43 -10
- package/dist/logger-Yz93KzdV.d.ts +30 -0
- package/dist/{metrics-BdBu5o8G.d.ts → metrics-DW80Exve.d.ts} +2 -2
- package/dist/origins-kvpxnVcW.d.ts +8 -0
- package/dist/prisma/index.d.ts +5 -5
- package/dist/prisma/index.js +130 -67
- package/dist/prisma/index.js.map +1 -1
- package/dist/{rateLimit-ZMIZJUZF.js → rateLimit-5RKBFLUR.js} +2 -2
- package/dist/server/auth/index.d.ts +82 -33
- package/dist/server/auth/index.js +273 -268
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/express/index.d.ts +9 -2
- package/dist/server/express/index.js +1 -1
- package/dist/server/index.d.ts +147 -67
- package/dist/server/index.js +520 -381
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +63 -17
- package/dist/server/mcp/index.js +256 -54
- package/dist/server/mcp/index.js.map +1 -1
- package/dist/server/otel.d.ts +3 -3
- package/dist/{sharing-DVa8W-yR.d.ts → sharing-UDLn1049.d.ts} +3 -3
- package/dist/testing/client.d.ts +14 -14
- package/dist/testing/client.js +9 -7
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts +12 -12
- package/dist/testing/index.js +21 -12
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock.d.ts +5 -5
- package/dist/testing/mock.js +8 -6
- package/dist/{types-Cu0vmx7c.d.ts → types-CYliTP65.d.ts} +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-2PWMUVC7.js.map +0 -1
- package/dist/chunk-4DIQHASK.js.map +0 -1
- package/dist/chunk-A6DJBGOG.js.map +0 -1
- package/dist/chunk-CDKG4TFA.js.map +0 -1
- package/dist/chunk-DD4SKCNO.js.map +0 -1
- package/dist/chunk-H23KEFBA.js.map +0 -1
- package/dist/chunk-JZBCLNDU.js.map +0 -1
- package/dist/chunk-KNDDCVQJ.js.map +0 -1
- package/dist/chunk-REX5YPHX.js.map +0 -1
- package/dist/chunk-RT6OILOI.js.map +0 -1
- package/dist/chunk-TBMKJ26R.js.map +0 -1
- package/dist/chunk-VJTFXHOK.js.map +0 -1
- package/dist/chunk-YSWH776Y.js +0 -13
- package/dist/chunk-YSWH776Y.js.map +0 -1
- package/dist/logger-BY28acW-.d.ts +0 -16
- /package/dist/{rateLimit-ZMIZJUZF.js.map → rateLimit-5RKBFLUR.js.map} +0 -0
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { A as AnyService, d as Caller } from '../../caller-
|
|
1
|
+
import { A as AnyService, d as Caller } from '../../caller-BT-Mv4bD.js';
|
|
2
2
|
import { IncomingHttpHeaders, IncomingMessage } from 'node:http';
|
|
3
|
-
import { ap as MethodName, d as StandardSchemaV1, c as InferOutput } from '../../infer-
|
|
3
|
+
import { ap as MethodName, d as StandardSchemaV1, c as InferOutput } from '../../infer-BmoZsL9a.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-
|
|
6
|
-
import { a as McpContext, b as McpContextOf, M as MaybePromise } from '../../types-
|
|
7
|
-
import { L as Logger } from '../../logger-
|
|
8
|
-
import { A as AuthenticateResult } from '../../auth-
|
|
5
|
+
import { P as PrincipalOfServices, C as ContractOfServices, D as Dispatcher, H as HttpRouter } from '../../http-DotQ5bj8.js';
|
|
6
|
+
import { a as McpContext, b as McpContextOf, M as MaybePromise } from '../../types-CYliTP65.js';
|
|
7
|
+
import { L as Logger } from '../../logger-Yz93KzdV.js';
|
|
8
|
+
import { A as AuthenticateResult } from '../../auth-DXiSJZaf.js';
|
|
9
9
|
import { Readable, Writable } from 'node:stream';
|
|
10
|
-
import '../../
|
|
11
|
-
import '../../
|
|
10
|
+
import { A as AllowedOrigin } from '../../origins-kvpxnVcW.js';
|
|
11
|
+
import '../../envelope-DzWyIXjj.js';
|
|
12
|
+
import '../../metrics-DW80Exve.js';
|
|
12
13
|
import 'socket.io';
|
|
13
14
|
|
|
14
15
|
/** The JSON Schema of a tool's arguments. MCP requires an object schema. */
|
|
@@ -55,7 +56,12 @@ interface McpStdioRequest {
|
|
|
55
56
|
/** The session's id. Every call of the server carries the same one. */
|
|
56
57
|
readonly sessionId: string;
|
|
57
58
|
}
|
|
58
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* What one request to the MCP HTTP routes stands for. Each request is a
|
|
61
|
+
* session of its own, and its queries share its caller's concurrency lane:
|
|
62
|
+
* its bearer token's, its user's without a token, or its address's when
|
|
63
|
+
* anonymous.
|
|
64
|
+
*/
|
|
59
65
|
interface McpHttpRequest {
|
|
60
66
|
readonly transport: "http";
|
|
61
67
|
readonly sessionId: string;
|
|
@@ -176,6 +182,14 @@ interface McpCustomTool<S extends readonly AnyService[] = readonly AnyService[],
|
|
|
176
182
|
* `"public"` lets anonymous callers in, with `principal: null`.
|
|
177
183
|
*/
|
|
178
184
|
readonly access?: McpToolAccess;
|
|
185
|
+
/**
|
|
186
|
+
* The kinds of principal (`principal.kind`) that may call the tool, within
|
|
187
|
+
* the ones the app's `initQuickdraw({ kinds })` admits, which it admits by
|
|
188
|
+
* default: a caller of another kind gets `FORBIDDEN` before the arguments
|
|
189
|
+
* are read or the handler runs. A `"public"` tool takes none of its own (a
|
|
190
|
+
* caller of a refused kind could call it signed out).
|
|
191
|
+
*/
|
|
192
|
+
readonly kinds?: readonly NonNullable<PrincipalOfServices<S>["kind"]>[];
|
|
179
193
|
/**
|
|
180
194
|
* The tool's arguments. A Standard Schema that can describe itself as JSON
|
|
181
195
|
* Schema (Zod 4.2 or later) validates them before the handler runs; a JSON
|
|
@@ -219,7 +233,13 @@ interface McpRegistryOptions<S extends readonly AnyService[], Tools extends read
|
|
|
219
233
|
* anonymous caller, who may call `"public"` methods and `"public"` custom
|
|
220
234
|
* tools only. A stdio server usually reads a token from its environment;
|
|
221
235
|
* an HTTP request carries one as a bearer token. Throwing fails the call
|
|
222
|
-
* with `UNAUTHENTICATED
|
|
236
|
+
* with `UNAUTHENTICATED`: throw `QuickdrawError("UNAUTHENTICATED")` (or
|
|
237
|
+
* `FORBIDDEN`) to refuse a token, which is logged at debug, and never put
|
|
238
|
+
* the token in a message; anything else thrown is logged as an error.
|
|
239
|
+
* Without it every call is anonymous. An HTTP request's queries share the
|
|
240
|
+
* lane of its bearer token: a hook that authenticates by anything else (a
|
|
241
|
+
* cookie, an API key header) must refuse a request whose bearer token it
|
|
242
|
+
* does not verify, or a caller makes a lane per made-up token.
|
|
223
243
|
*/
|
|
224
244
|
readonly principal?: (request: McpRequest) => MaybePromise<AuthenticateResult<PrincipalOfServices<S>>>;
|
|
225
245
|
/**
|
|
@@ -237,10 +257,15 @@ interface McpRegistryOptions<S extends readonly AnyService[], Tools extends read
|
|
|
237
257
|
* before the method runs when the caller is anonymous (`UNAUTHENTICATED`),
|
|
238
258
|
* or when the binder returns `undefined` or `null` or the caller sent the
|
|
239
259
|
* argument with another value (`FORBIDDEN`); a binder that throws fails it
|
|
240
|
-
* with `INTERNAL`, or with the code of a `QuickdrawError`.
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
260
|
+
* with `INTERNAL`, or with the code of a `QuickdrawError`. A call of any
|
|
261
|
+
* other method's tool that sends a bound argument is refused (`FORBIDDEN`).
|
|
262
|
+
* An input that is a `$ref` to an object counts as that object, and a
|
|
263
|
+
* bound key anywhere else in an input (in a union's or an intersection's
|
|
264
|
+
* branch, or an input that is no object) fails when the registry is built.
|
|
265
|
+
* Only top-level arguments are bound. Custom tools are never bound.
|
|
266
|
+
* Binding is not access control: the same principal reaches the method
|
|
267
|
+
* over a socket or HTTP with nothing bound, so the method's access must
|
|
268
|
+
* still confine it.
|
|
244
269
|
*/
|
|
245
270
|
readonly bind?: Readonly<Record<string, McpBinder<S>>>;
|
|
246
271
|
/** The app's own tools, listed after the generated ones. */
|
|
@@ -282,9 +307,14 @@ interface McpRegistry {
|
|
|
282
307
|
* `customTools`, served by `dispatcher`. Throws at once when a method's
|
|
283
308
|
* input schema cannot describe itself as JSON Schema (use Zod 4.2 or later
|
|
284
309
|
* for it, or `exclude` the method), when two tools share a name, when the
|
|
285
|
-
* dispatcher does not serve one of the services,
|
|
286
|
-
* `bind` is in no tool's object input or
|
|
287
|
-
* object
|
|
310
|
+
* dispatcher does not serve one of the services, when an argument of
|
|
311
|
+
* `bind` is in no tool's object input or may be anywhere in an input where
|
|
312
|
+
* it cannot be filled (an input that is not an object schema, or a branch
|
|
313
|
+
* of a union or an intersection, behind a `$ref` too), or when custom tools
|
|
314
|
+
* are served beside the services of apps that admit no kind in common.
|
|
315
|
+
* Every call runs in its caller's concurrency lane: a stdio session, or on
|
|
316
|
+
* HTTP its bearer token, its user without a token, or its address when
|
|
317
|
+
* anonymous.
|
|
288
318
|
*
|
|
289
319
|
* @example
|
|
290
320
|
* const registry = createMcpRegistry({
|
|
@@ -381,6 +411,22 @@ interface McpHttpRouterOptions {
|
|
|
381
411
|
readonly maxBodyBytes?: number;
|
|
382
412
|
/** Default: the console. */
|
|
383
413
|
readonly logger?: Logger;
|
|
414
|
+
/**
|
|
415
|
+
* The web pages that may call the routes, by origin: exact origins
|
|
416
|
+
* (`"https://app.example.com"`) or patterns anchored with `^` and `$`, as
|
|
417
|
+
* `socketAuth` takes them. A request whose `Origin` is not listed gets 403;
|
|
418
|
+
* a request without one (an MCP client, a script) is not a page's and
|
|
419
|
+
* passes. Default: none, so no page may call them.
|
|
420
|
+
*/
|
|
421
|
+
readonly allowedOrigins?: readonly AllowedOrigin[];
|
|
422
|
+
/**
|
|
423
|
+
* The `Host` names the routes answer for (`"localhost"`, `"127.0.0.1"`,
|
|
424
|
+
* `"mcp.example.com:8443"`; an entry without a port matches any port): a
|
|
425
|
+
* request for another gets 403. Set it for a server on a developer's
|
|
426
|
+
* machine or a LAN, where a page could reach it through DNS rebinding.
|
|
427
|
+
* Default: any host.
|
|
428
|
+
*/
|
|
429
|
+
readonly allowedHosts?: readonly string[];
|
|
384
430
|
}
|
|
385
431
|
/** What `invoke` answers: 4.1's `{ success, data }`, or a failure's message, code and data. */
|
|
386
432
|
type McpHttpReply = {
|
package/dist/server/mcp/index.js
CHANGED
|
@@ -3,20 +3,32 @@ import {
|
|
|
3
3
|
} from "../../chunk-XNLWBZIG.js";
|
|
4
4
|
import {
|
|
5
5
|
NEVER_ABORTED,
|
|
6
|
+
checkKind,
|
|
7
|
+
inputKeys,
|
|
6
8
|
lazyMembers,
|
|
9
|
+
narrowKinds,
|
|
7
10
|
parseInput,
|
|
11
|
+
pointerToken,
|
|
12
|
+
referenced,
|
|
13
|
+
runtimeOf,
|
|
8
14
|
toQuickdrawError
|
|
9
|
-
} from "../../chunk-
|
|
15
|
+
} from "../../chunk-MBH6IDSM.js";
|
|
10
16
|
import {
|
|
11
17
|
describeError
|
|
12
|
-
} from "../../chunk-
|
|
18
|
+
} from "../../chunk-LCC7HNFP.js";
|
|
13
19
|
import {
|
|
20
|
+
originAllowlist
|
|
21
|
+
} from "../../chunk-NO4M6PO5.js";
|
|
22
|
+
import {
|
|
23
|
+
isForbiddenCredential,
|
|
14
24
|
isJsonRequest,
|
|
25
|
+
isRefusal,
|
|
15
26
|
readJsonInput,
|
|
16
27
|
toPrincipal,
|
|
17
28
|
unreadable
|
|
18
|
-
} from "../../chunk-
|
|
19
|
-
import "../../chunk-
|
|
29
|
+
} from "../../chunk-RJ5I2VO3.js";
|
|
30
|
+
import "../../chunk-DUBU3BB3.js";
|
|
31
|
+
import "../../chunk-AK23XSWD.js";
|
|
20
32
|
import {
|
|
21
33
|
hasJsonSchema,
|
|
22
34
|
isStandardSchema
|
|
@@ -24,7 +36,7 @@ import {
|
|
|
24
36
|
import "../../chunk-Z2LZ7IZ7.js";
|
|
25
37
|
import {
|
|
26
38
|
consoleLogger
|
|
27
|
-
} from "../../chunk-
|
|
39
|
+
} from "../../chunk-DKE6QO26.js";
|
|
28
40
|
import {
|
|
29
41
|
INTERNAL_MESSAGE,
|
|
30
42
|
QuickdrawError,
|
|
@@ -72,20 +84,72 @@ function toolInputOf(label, schema, fail, instead = `leave ${label} out with exc
|
|
|
72
84
|
`the input schema of ${label} cannot be written as JSON Schema (${json.message}): change that schema, or ${instead}`
|
|
73
85
|
);
|
|
74
86
|
}
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
const described = referenced(json, json);
|
|
88
|
+
if (described?.type === "object") {
|
|
89
|
+
return { schema: inlined(json, described), mode: "object" };
|
|
77
90
|
}
|
|
78
|
-
const { $schema: dialect, ...input } = json;
|
|
91
|
+
const { $schema: dialect, definitions, $defs, ...input } = json;
|
|
79
92
|
return {
|
|
80
93
|
schema: {
|
|
81
94
|
...dialect === void 0 ? {} : { $schema: dialect },
|
|
82
95
|
type: "object",
|
|
83
|
-
properties: { input },
|
|
84
|
-
...acceptsUndefined(schema) ? {} : { required: ["input"] }
|
|
96
|
+
properties: { input: rerooted(input) },
|
|
97
|
+
...acceptsUndefined(schema) ? {} : { required: ["input"] },
|
|
98
|
+
...definitions === void 0 ? {} : { definitions: rerooted(definitions) },
|
|
99
|
+
...$defs === void 0 ? {} : { $defs: rerooted($defs) }
|
|
85
100
|
},
|
|
86
101
|
mode: "wrapped"
|
|
87
102
|
};
|
|
88
103
|
}
|
|
104
|
+
var WRAPPED_INPUT = "#/properties/input";
|
|
105
|
+
function rerooted(value) {
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
return value.map(rerooted);
|
|
108
|
+
}
|
|
109
|
+
if (!isRecord(value)) {
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
return Object.fromEntries(
|
|
113
|
+
Object.entries(value).map(([key, item]) => {
|
|
114
|
+
const intoRoot = key === "$ref" && typeof item === "string" && (item === "#" || item.startsWith("#/") && !/^#\/(?:definitions|\$defs)\//u.test(item));
|
|
115
|
+
return [key, intoRoot ? `${WRAPPED_INPUT}${item.slice(1)}` : rerooted(item)];
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
function refersTo(value, ref) {
|
|
120
|
+
if (Array.isArray(value)) {
|
|
121
|
+
return value.some((item) => refersTo(item, ref));
|
|
122
|
+
}
|
|
123
|
+
if (!isRecord(value)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return value.$ref === ref || Object.values(value).some((item) => refersTo(item, ref));
|
|
127
|
+
}
|
|
128
|
+
function inlined(json, target) {
|
|
129
|
+
if (target === json) {
|
|
130
|
+
return json;
|
|
131
|
+
}
|
|
132
|
+
const ref = String(json.$ref);
|
|
133
|
+
const { $schema: dialect } = json;
|
|
134
|
+
const schema = {
|
|
135
|
+
...dialect === void 0 ? {} : { $schema: dialect },
|
|
136
|
+
...target
|
|
137
|
+
};
|
|
138
|
+
for (const keyword of ["definitions", "$defs"]) {
|
|
139
|
+
const defs = json[keyword];
|
|
140
|
+
if (!isRecord(defs)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const others = Object.fromEntries(
|
|
144
|
+
Object.entries(defs).filter(([name]) => `#/${keyword}/${pointerToken(name)}` !== ref)
|
|
145
|
+
);
|
|
146
|
+
const kept = refersTo({ ...schema, [keyword]: others }, ref) ? defs : others;
|
|
147
|
+
if (Object.keys(kept).length > 0) {
|
|
148
|
+
schema[keyword] = kept;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return schema;
|
|
152
|
+
}
|
|
89
153
|
function inputOf(mode, args) {
|
|
90
154
|
if (mode === "object") {
|
|
91
155
|
return args ?? {};
|
|
@@ -139,35 +203,19 @@ function describeMethod(service, method, kind, text) {
|
|
|
139
203
|
function propertiesOf(schema) {
|
|
140
204
|
return isRecord(schema) && isRecord(schema.properties) ? Object.keys(schema.properties) : [];
|
|
141
205
|
}
|
|
142
|
-
function
|
|
143
|
-
if (
|
|
144
|
-
return
|
|
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));
|
|
206
|
+
function boundArguments(label, input, schema, bind, fail) {
|
|
207
|
+
if (bind.length === 0) {
|
|
208
|
+
return [];
|
|
161
209
|
}
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const misplaced = bind.find((key) =>
|
|
210
|
+
const bindable = input.mode === "object" ? propertiesOf(input.schema) : [];
|
|
211
|
+
const keys = inputKeys(schema) ?? [];
|
|
212
|
+
const misplaced = bind.find((key) => keys.includes(key) && !bindable.includes(key));
|
|
165
213
|
if (misplaced !== void 0) {
|
|
166
214
|
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`
|
|
215
|
+
`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
216
|
);
|
|
169
217
|
}
|
|
170
|
-
return
|
|
218
|
+
return bind.filter((key) => bindable.includes(key));
|
|
171
219
|
}
|
|
172
220
|
function withoutArguments(schema, bound) {
|
|
173
221
|
const kept = (key) => !bound.includes(key);
|
|
@@ -194,7 +242,7 @@ function planMethodTools(services, options, fail, bind = []) {
|
|
|
194
242
|
}
|
|
195
243
|
const label = `${service.name}.${method}`;
|
|
196
244
|
const input = toolInputOf(label, def.input, fail);
|
|
197
|
-
const bound = boundArguments(label, input, bind, fail);
|
|
245
|
+
const bound = boundArguments(label, input, def.input, bind, fail);
|
|
198
246
|
const tool = {
|
|
199
247
|
name: toolName(service.name, method),
|
|
200
248
|
description: describeMethod(service.name, method, def.kind, def.describe),
|
|
@@ -228,6 +276,46 @@ function describeTools(services, options = {}) {
|
|
|
228
276
|
return planned.map((entry2) => entry2.tool);
|
|
229
277
|
}
|
|
230
278
|
|
|
279
|
+
// src/server/mcp/lanes.ts
|
|
280
|
+
import { createHash } from "crypto";
|
|
281
|
+
import { isIPv6 } from "net";
|
|
282
|
+
function groupsOf(address) {
|
|
283
|
+
const written = new URL(`http://[${address}]/`).hostname.slice(1, -1);
|
|
284
|
+
const [head = "", tail = ""] = written.split("::");
|
|
285
|
+
const parse2 = (part) => part === "" ? [] : part.split(":").map((group) => Number.parseInt(group, 16));
|
|
286
|
+
const before = parse2(head);
|
|
287
|
+
const after = parse2(tail);
|
|
288
|
+
return [...before, ...new Array(8 - before.length - after.length).fill(0), ...after];
|
|
289
|
+
}
|
|
290
|
+
function addressLane(ip) {
|
|
291
|
+
const address = ip.split("%", 1)[0] ?? ip;
|
|
292
|
+
if (!isIPv6(address)) {
|
|
293
|
+
return ip;
|
|
294
|
+
}
|
|
295
|
+
const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0, g = 0, h = 0] = groupsOf(address);
|
|
296
|
+
if (a === 0 && b === 0 && c === 0 && d === 0 && e === 0 && f === 65535) {
|
|
297
|
+
return [g >> 8, g & 255, h >> 8, h & 255].join(".");
|
|
298
|
+
}
|
|
299
|
+
return `${[a, b, c, d & 65280].map((group) => group.toString(16)).join(":")}::/56`;
|
|
300
|
+
}
|
|
301
|
+
function addressOf(request) {
|
|
302
|
+
const { ip } = request.req;
|
|
303
|
+
return typeof ip === "string" && ip !== "" ? ip : request.req.socket?.remoteAddress ?? "unknown";
|
|
304
|
+
}
|
|
305
|
+
function laneOf(request, principal) {
|
|
306
|
+
if (request.transport === "stdio") {
|
|
307
|
+
return `mcp:${request.sessionId}`;
|
|
308
|
+
}
|
|
309
|
+
if (principal === null) {
|
|
310
|
+
return `mcp:http:address:${addressLane(addressOf(request))}`;
|
|
311
|
+
}
|
|
312
|
+
if (request.token === null) {
|
|
313
|
+
return `mcp:http:user:${principal.userId}`;
|
|
314
|
+
}
|
|
315
|
+
const hash = createHash("sha256").update(request.token).digest("base64url").slice(0, 22);
|
|
316
|
+
return `mcp:http:token:${hash}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
231
319
|
// src/server/mcp/session.ts
|
|
232
320
|
var CATEGORY = "quickdraw.mcp";
|
|
233
321
|
async function principalOf(settings, request) {
|
|
@@ -237,7 +325,8 @@ async function principalOf(settings, request) {
|
|
|
237
325
|
try {
|
|
238
326
|
return toPrincipal(await settings.principal(request));
|
|
239
327
|
} catch (error) {
|
|
240
|
-
|
|
328
|
+
const refused = isRefusal(error) || isForbiddenCredential(error);
|
|
329
|
+
settings.logger[refused ? "debug" : "error"]("MCP authentication failed", {
|
|
241
330
|
category: CATEGORY,
|
|
242
331
|
transport: request.transport,
|
|
243
332
|
error: describeError(error)
|
|
@@ -261,7 +350,7 @@ async function contextOf(settings, request, principal) {
|
|
|
261
350
|
async function openSession(settings, request) {
|
|
262
351
|
const principal = await principalOf(settings, request);
|
|
263
352
|
const mcp = await contextOf(settings, request, principal);
|
|
264
|
-
return { principal, mcp, connectionId:
|
|
353
|
+
return { principal, mcp, connectionId: laneOf(request, principal) };
|
|
265
354
|
}
|
|
266
355
|
function failure(settings, error, tool) {
|
|
267
356
|
const converted = toQuickdrawError(error);
|
|
@@ -342,7 +431,26 @@ function accessOf(owner, access, fail) {
|
|
|
342
431
|
}
|
|
343
432
|
return access;
|
|
344
433
|
}
|
|
345
|
-
function
|
|
434
|
+
function appKindsOf(services) {
|
|
435
|
+
let kinds;
|
|
436
|
+
for (const service of services) {
|
|
437
|
+
const own = runtimeOf(service)?.kinds;
|
|
438
|
+
if (own !== void 0) {
|
|
439
|
+
kinds = kinds === void 0 ? own : kinds.filter((kind) => own.includes(kind));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return kinds;
|
|
443
|
+
}
|
|
444
|
+
function kindsOf(owner, value, access, appKinds, fail) {
|
|
445
|
+
if (value.kinds !== void 0 && access === "public") {
|
|
446
|
+
fail(
|
|
447
|
+
`${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"`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const above = appKinds === void 0 ? void 0 : { kinds: appKinds, by: "initQuickdraw" };
|
|
451
|
+
return narrowKinds(value.kinds, above, (message) => fail(`${owner}: ${message}`));
|
|
452
|
+
}
|
|
453
|
+
function checkCustomTool(value, index, appKinds, fail) {
|
|
346
454
|
if (!isRecord2(value)) {
|
|
347
455
|
fail(`customTools[${index}] must be { name, description, inputSchema, handler }`);
|
|
348
456
|
}
|
|
@@ -361,6 +469,7 @@ function checkCustomTool(value, index, fail) {
|
|
|
361
469
|
fail(`${owner}: annotations must be an object of hints`);
|
|
362
470
|
}
|
|
363
471
|
const access = accessOf(owner, value.access, fail);
|
|
472
|
+
const kinds = kindsOf(owner, value, access, appKinds, fail);
|
|
364
473
|
const input = argumentsOf(owner, inputSchema, fail);
|
|
365
474
|
const tool = {
|
|
366
475
|
name,
|
|
@@ -371,19 +480,26 @@ function checkCustomTool(value, index, fail) {
|
|
|
371
480
|
return {
|
|
372
481
|
tool: Object.freeze(tool),
|
|
373
482
|
access,
|
|
483
|
+
kinds,
|
|
374
484
|
mode: input.mode,
|
|
375
485
|
schema: isStandardSchema(inputSchema) ? inputSchema : void 0,
|
|
376
486
|
handler
|
|
377
487
|
};
|
|
378
488
|
}
|
|
379
|
-
function checkCustomTools(value, fail) {
|
|
489
|
+
function checkCustomTools(value, fail, services = []) {
|
|
380
490
|
if (value === void 0) {
|
|
381
491
|
return [];
|
|
382
492
|
}
|
|
383
493
|
if (!Array.isArray(value)) {
|
|
384
494
|
fail("customTools must be an array of { name, description, inputSchema, handler }");
|
|
385
495
|
}
|
|
386
|
-
|
|
496
|
+
const appKinds = appKindsOf(services);
|
|
497
|
+
if (appKinds?.length === 0 && value.length > 0) {
|
|
498
|
+
fail(
|
|
499
|
+
"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"
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
return value.map((tool, index) => checkCustomTool(tool, index, appKinds, fail));
|
|
387
503
|
}
|
|
388
504
|
async function runCustomTool(settings, custom, args, session, options) {
|
|
389
505
|
const { name } = custom.tool;
|
|
@@ -392,6 +508,7 @@ async function runCustomTool(settings, custom, args, session, options) {
|
|
|
392
508
|
if (custom.access === "authenticated" && session.principal === null) {
|
|
393
509
|
throw new QuickdrawError("UNAUTHENTICATED", "Authentication required");
|
|
394
510
|
}
|
|
511
|
+
checkKind(custom.kinds, session.principal, `The tool "${name}"`);
|
|
395
512
|
const input = inputOf(custom.mode, args);
|
|
396
513
|
const parsed = custom.schema === void 0 ? input : await parseInput(custom.schema, input, `the tool "${name}"`);
|
|
397
514
|
const data = await custom.handler({
|
|
@@ -475,7 +592,7 @@ function resolveRegistry(options) {
|
|
|
475
592
|
const binders = checkBinders(options.bind, fail);
|
|
476
593
|
const methodTools = planMethodTools(options.services, options, fail, [...binders.keys()]);
|
|
477
594
|
checkBound(binders, methodTools, fail);
|
|
478
|
-
const customTools = checkCustomTools(options.customTools, fail);
|
|
595
|
+
const customTools = checkCustomTools(options.customTools, fail, options.services);
|
|
479
596
|
const entries = [
|
|
480
597
|
...methodTools.map((tool) => ({ kind: "method", ...tool })),
|
|
481
598
|
...customTools.map((tool) => ({ kind: "custom", ...tool }))
|
|
@@ -545,6 +662,16 @@ async function bindInput(settings, tool, input, session, request) {
|
|
|
545
662
|
return { ...given, ...Object.fromEntries(filled) };
|
|
546
663
|
}
|
|
547
664
|
async function dispatchMethod(settings, tool, input, session, options) {
|
|
665
|
+
const stray = settings.binders.size === 0 || !isRecord3(input) || Array.isArray(input) ? void 0 : [...settings.binders.keys()].find(
|
|
666
|
+
(key) => !tool.bound.includes(key) && Object.hasOwn(input, key)
|
|
667
|
+
);
|
|
668
|
+
if (stray !== void 0) {
|
|
669
|
+
const error = new QuickdrawError(
|
|
670
|
+
"FORBIDDEN",
|
|
671
|
+
`"${stray}" is bound to the caller; ${tool.service}.${tool.method} does not take it from the agent`
|
|
672
|
+
);
|
|
673
|
+
return settle(settings, options, failure(settings, error, tool.tool.name));
|
|
674
|
+
}
|
|
548
675
|
let bound = input;
|
|
549
676
|
if (tool.bound.length > 0) {
|
|
550
677
|
try {
|
|
@@ -894,8 +1021,81 @@ async function bootstrapMcpServer(serverModule) {
|
|
|
894
1021
|
}
|
|
895
1022
|
}
|
|
896
1023
|
|
|
897
|
-
// src/server/mcp/
|
|
1024
|
+
// src/server/mcp/httpRequest.ts
|
|
898
1025
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1026
|
+
function requestOf(req) {
|
|
1027
|
+
const header = req.headers.authorization;
|
|
1028
|
+
const token = typeof header === "string" && header.startsWith("Bearer ") ? header.slice(7) : "";
|
|
1029
|
+
return {
|
|
1030
|
+
transport: "http",
|
|
1031
|
+
sessionId: randomUUID2(),
|
|
1032
|
+
token: token.trim() === "" ? null : token.trim(),
|
|
1033
|
+
headers: req.headers,
|
|
1034
|
+
req
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
function hostOf(value) {
|
|
1038
|
+
if (/\s/u.test(value)) {
|
|
1039
|
+
return void 0;
|
|
1040
|
+
}
|
|
1041
|
+
const host = !value.startsWith("[") && value.split(":").length > 2 ? `[${value}]` : value;
|
|
1042
|
+
let url;
|
|
1043
|
+
try {
|
|
1044
|
+
url = new URL(`http://${host}`);
|
|
1045
|
+
} catch {
|
|
1046
|
+
return void 0;
|
|
1047
|
+
}
|
|
1048
|
+
const extra = url.username + url.password + url.search + url.hash;
|
|
1049
|
+
if (url.hostname === "" || extra !== "" || url.pathname !== "/") {
|
|
1050
|
+
return void 0;
|
|
1051
|
+
}
|
|
1052
|
+
const written = !host.endsWith("]") && /:\d+$/u.test(host);
|
|
1053
|
+
return { hostname: url.hostname, port: written ? url.port || "80" : void 0 };
|
|
1054
|
+
}
|
|
1055
|
+
function checkHosts(hosts) {
|
|
1056
|
+
if (hosts === void 0) {
|
|
1057
|
+
return void 0;
|
|
1058
|
+
}
|
|
1059
|
+
const parsed = Array.isArray(hosts) ? hosts.map((host) => typeof host === "string" ? hostOf(host) : void 0) : [];
|
|
1060
|
+
if (parsed.length === 0 || parsed.includes(void 0)) {
|
|
1061
|
+
throw new TypeError(
|
|
1062
|
+
'createMcpHttpRouter: allowedHosts must list host names, such as "localhost" or "mcp.example.com:8443"'
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
return Object.freeze(parsed);
|
|
1066
|
+
}
|
|
1067
|
+
function hostAllowed(hosts, header) {
|
|
1068
|
+
const host = header === void 0 ? void 0 : hostOf(header);
|
|
1069
|
+
if (host === void 0) {
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
const portMatches = (port) => host.port === void 0 ? port === "80" || port === "443" : port === host.port;
|
|
1073
|
+
return hosts.some(
|
|
1074
|
+
(entry2) => entry2.hostname === host.hostname && (entry2.port === void 0 || portMatches(entry2.port))
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
function requestGate(allowedOrigins, allowedHosts) {
|
|
1078
|
+
const origins = originAllowlist(
|
|
1079
|
+
allowedOrigins ?? [],
|
|
1080
|
+
"createMcpHttpRouter",
|
|
1081
|
+
true
|
|
1082
|
+
);
|
|
1083
|
+
const hosts = checkHosts(allowedHosts);
|
|
1084
|
+
return Object.freeze({
|
|
1085
|
+
refusal(req) {
|
|
1086
|
+
const { origin, host } = req.headers;
|
|
1087
|
+
if (origin !== void 0 && origins.allowed(origin) === null) {
|
|
1088
|
+
return "The MCP routes refuse requests from web pages unless allowedOrigins lists their origin";
|
|
1089
|
+
}
|
|
1090
|
+
if (hosts !== void 0 && !hostAllowed(hosts, host)) {
|
|
1091
|
+
return "The MCP routes do not answer for this host";
|
|
1092
|
+
}
|
|
1093
|
+
return void 0;
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/server/mcp/http.ts
|
|
899
1099
|
var DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
900
1100
|
var CATEGORY3 = "quickdraw.mcp";
|
|
901
1101
|
var NOT_JSON = "Send the call as a JSON body with Content-Type: application/json";
|
|
@@ -922,6 +1122,8 @@ function send(res, status, body) {
|
|
|
922
1122
|
res.statusCode = status;
|
|
923
1123
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
924
1124
|
res.setHeader("content-length", bytes);
|
|
1125
|
+
res.setHeader("cache-control", "no-store");
|
|
1126
|
+
res.setHeader("x-content-type-options", "nosniff");
|
|
925
1127
|
res.end(body);
|
|
926
1128
|
return bytes;
|
|
927
1129
|
}
|
|
@@ -962,17 +1164,6 @@ function targetOf(body) {
|
|
|
962
1164
|
}
|
|
963
1165
|
return void 0;
|
|
964
1166
|
}
|
|
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
1167
|
async function invoke(settings, req, res) {
|
|
977
1168
|
const controller = new AbortController();
|
|
978
1169
|
res.once("close", () => {
|
|
@@ -1022,7 +1213,8 @@ function resolveSettings(options) {
|
|
|
1022
1213
|
registry: options.registry,
|
|
1023
1214
|
prefix: normalizePath(options.path ?? "/mcp"),
|
|
1024
1215
|
maxBodyBytes,
|
|
1025
|
-
logger: options.logger ?? consoleLogger
|
|
1216
|
+
logger: options.logger ?? consoleLogger,
|
|
1217
|
+
gate: requestGate(options.allowedOrigins, options.allowedHosts)
|
|
1026
1218
|
};
|
|
1027
1219
|
}
|
|
1028
1220
|
function createMcpHttpRouter(options) {
|
|
@@ -1037,6 +1229,16 @@ function createMcpHttpRouter(options) {
|
|
|
1037
1229
|
}
|
|
1038
1230
|
return;
|
|
1039
1231
|
}
|
|
1232
|
+
const refusal = settings.gate.refusal(req);
|
|
1233
|
+
if (refusal !== void 0) {
|
|
1234
|
+
settings.logger.debug("An MCP HTTP request was refused", {
|
|
1235
|
+
category: CATEGORY3,
|
|
1236
|
+
origin: req.headers.origin,
|
|
1237
|
+
host: req.headers.host
|
|
1238
|
+
});
|
|
1239
|
+
sendResult(settings, res, failure2(new QuickdrawError("FORBIDDEN", refusal)));
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1040
1242
|
if (route === "tools") {
|
|
1041
1243
|
send(res, 200, JSON.stringify({ tools: settings.registry.tools }));
|
|
1042
1244
|
return;
|