@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.
- package/README.md +1308 -219
- package/dist/{admin-DKwkIh5i.d.ts → admin-CS7JB_UH.d.ts} +2 -2
- package/dist/{adminSchemas-2DUA_hZ8.d.ts → adminSchemas-29cGgrg5.d.ts} +6 -4
- package/dist/{auth-BhWqdyb-.d.ts → auth-DtdYjrZ9.d.ts} +51 -21
- package/dist/{caller-CXTFswBO.d.ts → caller-BTwGl7vm.d.ts} +296 -33
- package/dist/chunk-2FDSKLBO.js +82 -0
- package/dist/chunk-2FDSKLBO.js.map +1 -0
- package/dist/{chunk-RT6OILOI.js → chunk-4D2DOGAW.js} +2 -14
- package/dist/chunk-4D2DOGAW.js.map +1 -0
- package/dist/{chunk-OECZPRPW.js → chunk-64I6MYBC.js} +162 -775
- package/dist/chunk-64I6MYBC.js.map +1 -0
- package/dist/chunk-7473HK24.js +17 -0
- package/dist/chunk-7473HK24.js.map +1 -0
- package/dist/chunk-7Y5ROPXL.js +33 -0
- package/dist/chunk-7Y5ROPXL.js.map +1 -0
- package/dist/chunk-A2RBQD66.js +7 -0
- package/dist/chunk-A2RBQD66.js.map +1 -0
- package/dist/chunk-AK23XSWD.js +16 -0
- package/dist/chunk-AK23XSWD.js.map +1 -0
- package/dist/chunk-ASAMUGHG.js +651 -0
- package/dist/chunk-ASAMUGHG.js.map +1 -0
- package/dist/{chunk-B2P7AZ25.js → chunk-CYE4UBOB.js} +6 -4
- package/dist/chunk-CYE4UBOB.js.map +1 -0
- package/dist/chunk-DKE6QO26.js +136 -0
- package/dist/chunk-DKE6QO26.js.map +1 -0
- package/dist/chunk-DNP65TD5.js +832 -0
- package/dist/chunk-DNP65TD5.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-GYLWEJHW.js +19 -0
- package/dist/chunk-GYLWEJHW.js.map +1 -0
- package/dist/{chunk-SHEGLZEP.js → chunk-I3JCFS7H.js} +34 -13
- package/dist/chunk-I3JCFS7H.js.map +1 -0
- package/dist/{chunk-X5CWPX7H.js → chunk-IIQ3WMJK.js} +13 -2
- package/dist/chunk-IIQ3WMJK.js.map +1 -0
- package/dist/{chunk-RZTP4IEW.js → chunk-JVTONQFI.js} +6339 -4226
- package/dist/chunk-JVTONQFI.js.map +1 -0
- package/dist/{chunk-QEKVYG5F.js → chunk-KKQANRLG.js} +2 -2
- package/dist/chunk-KKQANRLG.js.map +1 -0
- package/dist/{chunk-CDKG4TFA.js → chunk-MPQAT55F.js} +38 -205
- package/dist/chunk-MPQAT55F.js.map +1 -0
- 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-NX2V6SIL.js +202 -0
- package/dist/chunk-NX2V6SIL.js.map +1 -0
- package/dist/{chunk-ZIVPHYRG.js → chunk-NZHA4TZD.js} +72 -175
- package/dist/chunk-NZHA4TZD.js.map +1 -0
- package/dist/{chunk-2PWMUVC7.js → chunk-OCZJ4EYV.js} +518 -27
- package/dist/chunk-OCZJ4EYV.js.map +1 -0
- package/dist/{chunk-REX5YPHX.js → chunk-PHCURHC7.js} +76 -21
- package/dist/chunk-PHCURHC7.js.map +1 -0
- package/dist/{chunk-4YO5H5ON.js → chunk-Q4OGQM5I.js} +4 -10
- package/dist/chunk-Q4OGQM5I.js.map +1 -0
- package/dist/chunk-QSU47XEX.js +13 -0
- package/dist/chunk-QSU47XEX.js.map +1 -0
- package/dist/{chunk-DD4SKCNO.js → chunk-RZXJTDH5.js} +13 -2
- package/dist/chunk-RZXJTDH5.js.map +1 -0
- package/dist/{chunk-Z2LZ7IZ7.js → chunk-SN3OZVEQ.js} +5 -31
- package/dist/chunk-SN3OZVEQ.js.map +1 -0
- package/dist/{chunk-4DIQHASK.js → chunk-SQ5VUG77.js} +28 -8
- package/dist/chunk-SQ5VUG77.js.map +1 -0
- package/dist/chunk-STH5DLQP.js +1192 -0
- package/dist/chunk-STH5DLQP.js.map +1 -0
- package/dist/{chunk-QCHKWIRS.js → chunk-VZFJTKIW.js} +13 -13
- package/dist/{chunk-QCHKWIRS.js.map → chunk-VZFJTKIW.js.map} +1 -1
- package/dist/{chunk-VSQBHPWH.js → chunk-X4ZU4QH5.js} +54 -22
- package/dist/chunk-X4ZU4QH5.js.map +1 -0
- package/dist/chunk-XD7SRDUY.js +188 -0
- package/dist/chunk-XD7SRDUY.js.map +1 -0
- package/dist/chunk-XH2IZVLT.js +62 -0
- package/dist/chunk-XH2IZVLT.js.map +1 -0
- package/dist/chunk-Z7I6SONO.js +825 -0
- package/dist/chunk-Z7I6SONO.js.map +1 -0
- package/dist/cli/quickdraw-docs.js +132 -112
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/core.d.ts +387 -0
- package/dist/client/core.js +109 -0
- package/dist/client/index.d.ts +52 -203
- package/dist/client/index.js +59 -218
- package/dist/client/index.js.map +1 -1
- package/dist/{clientTypes-Bc-G8n-U.d.ts → clientTypes-CUUNJwWs.d.ts} +3 -3
- package/dist/{connection-Cr1BU07W.d.ts → connection-2EeQ_Et0.d.ts} +22 -11
- package/dist/{createServer-CP9eDHZZ.d.ts → createServer-Bx5HOcYK.d.ts} +67 -17
- package/dist/{createTestApp-UKVGEmOf.d.ts → createTestApp-DAMANT79.d.ts} +46 -13
- package/dist/{envelope-Cp3YcVxC.d.ts → envelope-BMBaxppV.d.ts} +216 -10
- package/dist/{http-Djj2Inuu.d.ts → http-YzLAar3w.d.ts} +76 -60
- package/dist/index.d.ts +8 -7
- package/dist/index.js +82 -25
- package/dist/index.js.map +1 -1
- package/dist/{infer-Teo1xpsd.d.ts → infer-Br62cnyW.d.ts} +82 -11
- package/dist/logger-Yz93KzdV.d.ts +30 -0
- package/dist/{metrics-BdBu5o8G.d.ts → metrics-CTVpw0gu.d.ts} +2 -2
- package/dist/origins-kvpxnVcW.d.ts +8 -0
- package/dist/parser.js +2 -1
- package/dist/prisma/index.d.ts +14 -7
- package/dist/prisma/index.js +279 -70
- package/dist/prisma/index.js.map +1 -1
- package/dist/{rateLimit-ZMIZJUZF.js → rateLimit-5RKBFLUR.js} +2 -2
- package/dist/rateLimit-5RKBFLUR.js.map +1 -0
- package/dist/remote-N7OUAGOW.js +15 -0
- package/dist/remote-N7OUAGOW.js.map +1 -0
- package/dist/server/auth/index.d.ts +82 -33
- package/dist/server/auth/index.js +277 -270
- 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 +268 -84
- package/dist/server/index.js +664 -418
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +89 -27
- package/dist/server/mcp/index.js +281 -62
- 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-BrsmlDv_.d.ts} +3 -3
- package/dist/testing/client.d.ts +14 -14
- package/dist/testing/client.js +19 -10
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts +50 -13
- package/dist/testing/index.js +113 -21
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock.d.ts +5 -5
- package/dist/testing/mock.js +17 -9
- package/dist/tracked-BR5b2O-U.d.ts +43 -0
- package/dist/{types-Cu0vmx7c.d.ts → types-CLRCQGnO.d.ts} +4 -3
- package/dist/types-tMagHbjy.d.ts +151 -0
- package/dist/utils/index.d.ts +16 -4
- package/dist/utils/index.js +6 -3
- package/package.json +5 -1
- package/dist/chunk-2PWMUVC7.js.map +0 -1
- package/dist/chunk-4DIQHASK.js.map +0 -1
- package/dist/chunk-4YO5H5ON.js.map +0 -1
- package/dist/chunk-B2P7AZ25.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-OECZPRPW.js.map +0 -1
- package/dist/chunk-QEKVYG5F.js.map +0 -1
- package/dist/chunk-REX5YPHX.js.map +0 -1
- package/dist/chunk-RT6OILOI.js.map +0 -1
- package/dist/chunk-RZTP4IEW.js.map +0 -1
- package/dist/chunk-SHEGLZEP.js.map +0 -1
- package/dist/chunk-TBMKJ26R.js.map +0 -1
- package/dist/chunk-VSQBHPWH.js.map +0 -1
- package/dist/chunk-X5CWPX7H.js.map +0 -1
- package/dist/chunk-YSWH776Y.js +0 -13
- package/dist/chunk-YSWH776Y.js.map +0 -1
- package/dist/chunk-Z2LZ7IZ7.js.map +0 -1
- package/dist/chunk-ZIVPHYRG.js.map +0 -1
- package/dist/logger-BY28acW-.d.ts +0 -16
- /package/dist/{rateLimit-ZMIZJUZF.js.map → client/core.js.map} +0 -0
package/dist/server/mcp/index.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
12
|
+
pointerToken,
|
|
13
|
+
referenced,
|
|
14
|
+
runtimeOf,
|
|
15
|
+
toQuickdrawError,
|
|
16
|
+
withCallerGrants
|
|
17
|
+
} from "../../chunk-OCZJ4EYV.js";
|
|
10
18
|
import {
|
|
11
19
|
describeError
|
|
12
|
-
} from "../../chunk-
|
|
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-
|
|
19
|
-
import "../../chunk-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
|
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));
|
|
211
|
+
function boundArguments(label, input, schema, bind, fail) {
|
|
212
|
+
if (bind.length === 0) {
|
|
213
|
+
return [];
|
|
161
214
|
}
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const misplaced = bind.find((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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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/
|
|
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;
|