machine-bridge-mcp 3.0.0-beta.21 → 3.0.0-beta.26
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/CHANGELOG.md +134 -0
- package/CONTRIBUTING.md +3 -3
- package/GOVERNANCE.md +2 -2
- package/README.md +24 -6
- package/browser-extension/manifest.json +1 -1
- package/docs/AGENT_CONTEXT.md +10 -7
- package/docs/ARCHITECTURE.md +35 -22
- package/docs/AUDIT.md +85 -1
- package/docs/CLIENTS.md +6 -2
- package/docs/ENGINEERING.md +31 -9
- package/docs/LOCAL_AUTOMATION.md +4 -2
- package/docs/LOGGING.md +8 -8
- package/docs/OPERATIONS.md +43 -17
- package/docs/PRIVACY.md +18 -4
- package/docs/PROJECT_STANDARDS.md +2 -2
- package/docs/RELEASING.md +35 -11
- package/docs/TESTING.md +36 -16
- package/docs/THREAT_MODEL.md +20 -5
- package/docs/TOOL_REFERENCE.md +18 -12
- package/docs/UPGRADING.md +32 -0
- package/package.json +15 -6
- package/scripts/check-plan.mjs +8 -0
- package/scripts/coverage-check.mjs +30 -1
- package/scripts/foreground-daemon-recovery.mjs +88 -0
- package/scripts/github-release.mjs +22 -16
- package/scripts/install-published-prerelease.mjs +7 -7
- package/scripts/official-mcp-conformance.mjs +243 -0
- package/scripts/persistent-activation-process.mjs +36 -0
- package/scripts/release-candidate-manifest.mjs +12 -0
- package/scripts/release-publication-guard.mjs +65 -0
- package/scripts/release-state.mjs +1 -1
- package/scripts/sbom-check.mjs +99 -0
- package/scripts/start-release-candidate.mjs +39 -13
- package/src/local/agent-context-projection.mjs +26 -7
- package/src/local/agent-context.mjs +25 -4
- package/src/local/autostart-log-maintenance.mjs +36 -0
- package/src/local/capability-observer.mjs +5 -0
- package/src/local/child-process-settlement.mjs +103 -0
- package/src/local/cli-activate.mjs +42 -5
- package/src/local/cli-service.mjs +55 -5
- package/src/local/cli.mjs +59 -10
- package/src/local/daemon-process.mjs +24 -3
- package/src/local/delegated-process-sandbox.mjs +1 -0
- package/src/local/execution-routing.mjs +231 -0
- package/src/local/git-service.mjs +3 -1
- package/src/local/job-runner.mjs +55 -19
- package/src/local/macos-trust-broker.mjs +7 -0
- package/src/local/managed-job-runner-claim.mjs +54 -0
- package/src/local/managed-job-runner.mjs +13 -2
- package/src/local/process-execution.mjs +2 -2
- package/src/local/process-identity.mjs +11 -0
- package/src/local/process-tree-ownership-types.d.ts +37 -0
- package/src/local/process-tree-ownership.mjs +49 -41
- package/src/local/process-tree.mjs +1 -1
- package/src/local/relay-call-recovery.mjs +40 -21
- package/src/local/runtime-activation.mjs +357 -38
- package/src/local/runtime-capabilities.mjs +22 -6
- package/src/local/runtime-diagnostics.mjs +9 -2
- package/src/local/runtime.mjs +18 -4
- package/src/local/service-convergence.mjs +33 -0
- package/src/local/service-owner.mjs +147 -0
- package/src/local/service-restart-handoff.mjs +22 -8
- package/src/local/service-runtime.mjs +145 -0
- package/src/local/service.mjs +143 -25
- package/src/local/state.mjs +104 -7
- package/src/local/stdio.mjs +139 -45
- package/src/local/system-network-route.mjs +76 -0
- package/src/local/tool-executor.mjs +24 -6
- package/src/local/tools.mjs +6 -5
- package/src/local/windows-service-convergence.mjs +49 -0
- package/src/local/windows-service.mjs +30 -53
- package/src/shared/mcp-protocol.d.mts +27 -0
- package/src/shared/mcp-protocol.mjs +256 -0
- package/src/shared/mcp-subscriptions.d.mts +4 -0
- package/src/shared/mcp-subscriptions.mjs +59 -0
- package/src/shared/relay-contract.json +1 -0
- package/src/shared/result-projection.d.mts +2 -1
- package/src/shared/result-projection.mjs +13 -2
- package/src/shared/server-metadata.json +11 -4
- package/src/shared/tool-argument-validation.d.mts +17 -0
- package/src/shared/tool-argument-validation.mjs +325 -0
- package/src/shared/tool-catalog.json +18 -12
- package/src/worker/durable-stream-calls.ts +12 -24
- package/src/worker/http.ts +36 -2
- package/src/worker/index.ts +181 -165
- package/src/worker/mcp-http-contract.ts +276 -0
- package/src/worker/mcp-jsonrpc.ts +12 -6
- package/src/worker/mcp-legacy-dispatch.ts +104 -0
- package/src/worker/mcp-modern-controller.ts +199 -0
- package/src/worker/mcp-modern-proxy.ts +126 -0
- package/src/worker/mcp-modern-stream.ts +71 -0
- package/src/worker/mcp-session.ts +12 -3
- package/src/worker/mcp-stream-proxy-contract.ts +67 -0
- package/src/worker/mcp-stream-proxy.ts +17 -60
- package/src/worker/mcp-tool-call-input.ts +23 -0
- package/src/worker/tool-catalog.ts +29 -1
- package/src/worker/tool-timeout.ts +53 -12
- package/src/worker/worker-mcp-config.ts +23 -0
- package/src/worker/worker-metadata.ts +10 -1
- package/src/worker/worker-runtime-config.ts +19 -0
- package/src/worker/worker-static-routes.ts +7 -2
- package/tsconfig.local.json +7 -1
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
export const JSON_SCHEMA_2020_12 = "https://json-schema.org/draft/2020-12/schema";
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_TYPES = new Set(["null", "boolean", "object", "array", "number", "integer", "string"]);
|
|
4
|
+
const SUPPORTED_KEYWORDS = new Set([
|
|
5
|
+
"$schema", "type", "enum", "const", "default", "title", "description",
|
|
6
|
+
"properties", "required", "additionalProperties", "items",
|
|
7
|
+
"minItems", "maxItems", "minLength", "maxLength", "pattern",
|
|
8
|
+
"minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf",
|
|
9
|
+
"minProperties", "maxProperties", "allOf", "anyOf", "oneOf", "not",
|
|
10
|
+
"if", "then", "else", "x-mcp-header",
|
|
11
|
+
]);
|
|
12
|
+
const DEFAULT_LIMITS = Object.freeze({
|
|
13
|
+
maximumDepth: 32, maximumNodes: 4096, maximumIssues: 16, maximumPatternLength: 2048, maximumValidationSteps: 65_536,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export class ToolSchemaContractError extends Error {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "ToolSchemaContractError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ToolArgumentValidationError extends Error {
|
|
24
|
+
constructor(tool, issues) {
|
|
25
|
+
super(`tool arguments do not match the input schema: ${tool}`);
|
|
26
|
+
this.name = "ToolArgumentValidationError";
|
|
27
|
+
this.code = "invalid_request";
|
|
28
|
+
this.retryable = false;
|
|
29
|
+
this.details = Object.freeze({
|
|
30
|
+
tool: String(tool),
|
|
31
|
+
validation_issues: Object.freeze(issues.map((issue) => Object.freeze({ ...issue }))),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function compileToolArgumentValidators(tools, options = {}) {
|
|
37
|
+
if (!Array.isArray(tools)) throw new ToolSchemaContractError("tool catalog must be an array");
|
|
38
|
+
const limits = normalizeLimits(options);
|
|
39
|
+
const state = { nodes: 0, limits };
|
|
40
|
+
const validators = new Map();
|
|
41
|
+
for (const tool of tools) {
|
|
42
|
+
if (!isRecord(tool) || typeof tool.name !== "string" || !tool.name) {
|
|
43
|
+
throw new ToolSchemaContractError("tool catalog contains an invalid tool definition");
|
|
44
|
+
}
|
|
45
|
+
if (validators.has(tool.name)) throw new ToolSchemaContractError(`duplicate tool schema: ${tool.name}`);
|
|
46
|
+
const schema = tool.inputSchema ?? { type: "object" };
|
|
47
|
+
const compiled = compileSchema(schema, `tool ${tool.name}.inputSchema`, 0, state);
|
|
48
|
+
if (!compiled.types?.includes("object")) {
|
|
49
|
+
throw new ToolSchemaContractError(`tool ${tool.name} input schema must declare type object`);
|
|
50
|
+
}
|
|
51
|
+
validators.set(tool.name, compiled);
|
|
52
|
+
}
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
names: Object.freeze([...validators.keys()]),
|
|
55
|
+
has(tool) { return validators.has(String(tool)); },
|
|
56
|
+
validate(tool, value) {
|
|
57
|
+
const name = String(tool || "");
|
|
58
|
+
const schema = validators.get(name);
|
|
59
|
+
if (!schema) return Object.freeze({ known: false, valid: false, issues: Object.freeze([]) });
|
|
60
|
+
const issues = [];
|
|
61
|
+
const budget = { remaining: limits.maximumValidationSteps };
|
|
62
|
+
try { validateNode(schema, value, "", issues, limits.maximumIssues, budget); }
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error !== VALIDATION_BUDGET_EXCEEDED) throw error;
|
|
65
|
+
pushIssue(issues, "", "validationBudget", "validation exceeded the bounded work budget", limits.maximumIssues);
|
|
66
|
+
}
|
|
67
|
+
return Object.freeze({ known: true, valid: issues.length === 0, issues: Object.freeze(issues) });
|
|
68
|
+
},
|
|
69
|
+
assert(tool, value) {
|
|
70
|
+
const result = this.validate(tool, value);
|
|
71
|
+
if (!result.known) throw new ToolSchemaContractError("unknown tool schema");
|
|
72
|
+
if (!result.valid) throw new ToolArgumentValidationError(tool, result.issues);
|
|
73
|
+
return value;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function compileSchema(schema, label, depth, state) {
|
|
79
|
+
if (schema === true) return { boolean: true };
|
|
80
|
+
if (schema === false) return { boolean: false };
|
|
81
|
+
if (!isRecord(schema)) throw new ToolSchemaContractError(`${label} must be an object or boolean schema`);
|
|
82
|
+
if (depth > state.limits.maximumDepth) throw new ToolSchemaContractError(`${label} exceeds maximum schema depth`);
|
|
83
|
+
state.nodes += 1;
|
|
84
|
+
if (state.nodes > state.limits.maximumNodes) throw new ToolSchemaContractError("tool catalog exceeds maximum schema node count");
|
|
85
|
+
for (const key of Object.keys(schema)) {
|
|
86
|
+
if (!SUPPORTED_KEYWORDS.has(key)) throw new ToolSchemaContractError(`${label} uses unsupported JSON Schema keyword: ${key}`);
|
|
87
|
+
}
|
|
88
|
+
validateDialect(schema.$schema, label);
|
|
89
|
+
const types = compileTypes(schema.type, label);
|
|
90
|
+
const node = { types };
|
|
91
|
+
if (Object.hasOwn(schema, "const")) node.constValue = structuredClone(schema.const);
|
|
92
|
+
if (Object.hasOwn(schema, "enum")) node.enumValues = compileEnum(schema.enum, label);
|
|
93
|
+
if (schema.pattern !== undefined) {
|
|
94
|
+
if (typeof schema.pattern !== "string" || schema.pattern.length > state.limits.maximumPatternLength) {
|
|
95
|
+
throw new ToolSchemaContractError(`${label}.pattern is invalid or too large`);
|
|
96
|
+
}
|
|
97
|
+
try { node.pattern = new RegExp(schema.pattern, "u"); }
|
|
98
|
+
catch { throw new ToolSchemaContractError(`${label}.pattern is not a valid ECMAScript regular expression`); }
|
|
99
|
+
}
|
|
100
|
+
for (const key of ["minLength", "maxLength", "minItems", "maxItems", "minProperties", "maxProperties"]) {
|
|
101
|
+
if (schema[key] !== undefined) node[key] = nonNegativeInteger(schema[key], `${label}.${key}`);
|
|
102
|
+
}
|
|
103
|
+
for (const key of ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"]) {
|
|
104
|
+
if (schema[key] !== undefined) node[key] = finiteNumber(schema[key], `${label}.${key}`);
|
|
105
|
+
}
|
|
106
|
+
if (schema.multipleOf !== undefined) {
|
|
107
|
+
node.multipleOf = finiteNumber(schema.multipleOf, `${label}.multipleOf`);
|
|
108
|
+
if (node.multipleOf <= 0) throw new ToolSchemaContractError(`${label}.multipleOf must be greater than zero`);
|
|
109
|
+
}
|
|
110
|
+
if (schema.properties !== undefined) {
|
|
111
|
+
if (!isRecord(schema.properties)) throw new ToolSchemaContractError(`${label}.properties must be an object`);
|
|
112
|
+
node.properties = new Map(Object.entries(schema.properties).map(([key, child]) => [
|
|
113
|
+
key, compileSchema(child, `${label}.properties.${key}`, depth + 1, state),
|
|
114
|
+
]));
|
|
115
|
+
}
|
|
116
|
+
if (schema.required !== undefined) {
|
|
117
|
+
if (!Array.isArray(schema.required) || !schema.required.every((value) => typeof value === "string")) {
|
|
118
|
+
throw new ToolSchemaContractError(`${label}.required must be an array of strings`);
|
|
119
|
+
}
|
|
120
|
+
if (new Set(schema.required).size !== schema.required.length) throw new ToolSchemaContractError(`${label}.required contains duplicates`);
|
|
121
|
+
node.required = Object.freeze([...schema.required]);
|
|
122
|
+
}
|
|
123
|
+
if (schema.additionalProperties !== undefined) {
|
|
124
|
+
node.additionalProperties = typeof schema.additionalProperties === "boolean"
|
|
125
|
+
? schema.additionalProperties
|
|
126
|
+
: compileSchema(schema.additionalProperties, `${label}.additionalProperties`, depth + 1, state);
|
|
127
|
+
}
|
|
128
|
+
if (schema.items !== undefined) node.items = compileSchema(schema.items, `${label}.items`, depth + 1, state);
|
|
129
|
+
for (const key of ["allOf", "anyOf", "oneOf"]) {
|
|
130
|
+
if (schema[key] !== undefined) {
|
|
131
|
+
if (!Array.isArray(schema[key]) || schema[key].length === 0) throw new ToolSchemaContractError(`${label}.${key} must be a non-empty array`);
|
|
132
|
+
node[key] = schema[key].map((child, index) => compileSchema(child, `${label}.${key}[${index}]`, depth + 1, state));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const key of ["not", "if", "then", "else"]) {
|
|
136
|
+
if (schema[key] !== undefined) node[key] = compileSchema(schema[key], `${label}.${key}`, depth + 1, state);
|
|
137
|
+
}
|
|
138
|
+
return node;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const VALIDATION_BUDGET_EXCEEDED = Object.freeze({ code: "validation_budget_exceeded" });
|
|
142
|
+
|
|
143
|
+
function validateNode(node, value, instancePath, issues, maximumIssues, budget) {
|
|
144
|
+
if (issues.length >= maximumIssues) return;
|
|
145
|
+
if (budget.remaining <= 0) throw VALIDATION_BUDGET_EXCEEDED;
|
|
146
|
+
budget.remaining -= 1;
|
|
147
|
+
if (node.boolean === true) return;
|
|
148
|
+
if (node.boolean === false) return pushIssue(issues, instancePath, "falseSchema", "value is rejected by the schema", maximumIssues);
|
|
149
|
+
if (node.types && !node.types.some((type) => matchesType(type, value))) {
|
|
150
|
+
pushIssue(issues, instancePath, "type", `must be ${node.types.join(" or ")}`, maximumIssues);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (node.constValue !== undefined && !jsonEqual(value, node.constValue)) {
|
|
154
|
+
pushIssue(issues, instancePath, "const", "must equal the declared constant", maximumIssues);
|
|
155
|
+
}
|
|
156
|
+
if (node.enumValues && !node.enumValues.some((candidate) => jsonEqual(value, candidate))) {
|
|
157
|
+
pushIssue(issues, instancePath, "enum", "must equal one of the allowed values", maximumIssues);
|
|
158
|
+
}
|
|
159
|
+
if (typeof value === "string") validateString(node, value, instancePath, issues, maximumIssues);
|
|
160
|
+
if (typeof value === "number") validateNumber(node, value, instancePath, issues, maximumIssues);
|
|
161
|
+
if (Array.isArray(value)) validateArray(node, value, instancePath, issues, maximumIssues, budget);
|
|
162
|
+
else if (isRecord(value)) validateObject(node, value, instancePath, issues, maximumIssues, budget);
|
|
163
|
+
validateComposition(node, value, instancePath, issues, maximumIssues, budget);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function validateString(node, value, path, issues, maximumIssues) {
|
|
167
|
+
if (node.minLength !== undefined || node.maxLength !== undefined) {
|
|
168
|
+
const length = boundedCodePointLength(value, node.minLength, node.maxLength);
|
|
169
|
+
if (node.minLength !== undefined && length < node.minLength) pushIssue(issues, path, "minLength", `must contain at least ${node.minLength} characters`, maximumIssues);
|
|
170
|
+
if (node.maxLength !== undefined && length > node.maxLength) pushIssue(issues, path, "maxLength", `must contain at most ${node.maxLength} characters`, maximumIssues);
|
|
171
|
+
}
|
|
172
|
+
if (node.pattern && !node.pattern.test(value)) pushIssue(issues, path, "pattern", "must match the declared pattern", maximumIssues);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function boundedCodePointLength(value, minimum, maximum) {
|
|
176
|
+
const stopAt = maximum !== undefined ? maximum + 1 : minimum;
|
|
177
|
+
let length = 0;
|
|
178
|
+
for (let offset = 0; offset < value.length && (stopAt === undefined || length < stopAt); length += 1) {
|
|
179
|
+
const point = value.codePointAt(offset);
|
|
180
|
+
offset += point > 0xFFFF ? 2 : 1;
|
|
181
|
+
}
|
|
182
|
+
return length;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function validateNumber(node, value, path, issues, maximumIssues) {
|
|
186
|
+
if (!Number.isFinite(value)) return pushIssue(issues, path, "type", "must be a finite number", maximumIssues);
|
|
187
|
+
if (node.minimum !== undefined && value < node.minimum) pushIssue(issues, path, "minimum", `must be at least ${node.minimum}`, maximumIssues);
|
|
188
|
+
if (node.maximum !== undefined && value > node.maximum) pushIssue(issues, path, "maximum", `must be at most ${node.maximum}`, maximumIssues);
|
|
189
|
+
if (node.exclusiveMinimum !== undefined && value <= node.exclusiveMinimum) pushIssue(issues, path, "exclusiveMinimum", `must be greater than ${node.exclusiveMinimum}`, maximumIssues);
|
|
190
|
+
if (node.exclusiveMaximum !== undefined && value >= node.exclusiveMaximum) pushIssue(issues, path, "exclusiveMaximum", `must be less than ${node.exclusiveMaximum}`, maximumIssues);
|
|
191
|
+
if (node.multipleOf !== undefined && !isMultipleOf(value, node.multipleOf)) pushIssue(issues, path, "multipleOf", `must be a multiple of ${node.multipleOf}`, maximumIssues);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function validateArray(node, value, path, issues, maximumIssues, budget) {
|
|
195
|
+
if (node.minItems !== undefined && value.length < node.minItems) pushIssue(issues, path, "minItems", `must contain at least ${node.minItems} items`, maximumIssues);
|
|
196
|
+
if (node.maxItems !== undefined && value.length > node.maxItems) pushIssue(issues, path, "maxItems", `must contain at most ${node.maxItems} items`, maximumIssues);
|
|
197
|
+
if (node.items) value.forEach((item, index) => validateNode(node.items, item, `${path}/${index}`, issues, maximumIssues, budget));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function validateObject(node, value, path, issues, maximumIssues, budget) {
|
|
201
|
+
for (const required of node.required ?? []) {
|
|
202
|
+
if (!Object.hasOwn(value, required)) pushIssue(issues, path, "required", `missing required property ${required}`, maximumIssues);
|
|
203
|
+
}
|
|
204
|
+
let propertyCount = 0;
|
|
205
|
+
for (const key in value) {
|
|
206
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
207
|
+
if (budget.remaining <= 0) throw VALIDATION_BUDGET_EXCEEDED;
|
|
208
|
+
budget.remaining -= 1;
|
|
209
|
+
propertyCount += 1;
|
|
210
|
+
const child = node.properties?.get(key);
|
|
211
|
+
if (child) validateNode(child, value[key], `${path}/${escapePointer(key)}`, issues, maximumIssues, budget);
|
|
212
|
+
else if (node.additionalProperties === false) pushIssue(issues, `${path}/${escapePointer(key)}`, "additionalProperties", "property is not allowed", maximumIssues);
|
|
213
|
+
else if (node.additionalProperties && node.additionalProperties !== true) validateNode(node.additionalProperties, value[key], `${path}/${escapePointer(key)}`, issues, maximumIssues, budget);
|
|
214
|
+
}
|
|
215
|
+
if (node.minProperties !== undefined && propertyCount < node.minProperties) pushIssue(issues, path, "minProperties", `must contain at least ${node.minProperties} properties`, maximumIssues);
|
|
216
|
+
if (node.maxProperties !== undefined && propertyCount > node.maxProperties) pushIssue(issues, path, "maxProperties", `must contain at most ${node.maxProperties} properties`, maximumIssues);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function validateComposition(node, value, path, issues, maximumIssues, budget) {
|
|
220
|
+
if (node.allOf) for (const child of node.allOf) validateNode(child, value, path, issues, maximumIssues, budget);
|
|
221
|
+
if (node.anyOf && !node.anyOf.some((child) => isValid(child, value, budget))) pushIssue(issues, path, "anyOf", "must match at least one schema", maximumIssues);
|
|
222
|
+
if (node.oneOf) {
|
|
223
|
+
const matches = node.oneOf.reduce((count, child) => count + Number(isValid(child, value, budget)), 0);
|
|
224
|
+
if (matches !== 1) pushIssue(issues, path, "oneOf", "must match exactly one schema", maximumIssues);
|
|
225
|
+
}
|
|
226
|
+
if (node.not && isValid(node.not, value, budget)) pushIssue(issues, path, "not", "must not match the excluded schema", maximumIssues);
|
|
227
|
+
if (node.if) {
|
|
228
|
+
const branch = isValid(node.if, value, budget) ? node.then : node.else;
|
|
229
|
+
if (branch) validateNode(branch, value, path, issues, maximumIssues, budget);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isValid(node, value, budget) {
|
|
234
|
+
const issues = [];
|
|
235
|
+
validateNode(node, value, "", issues, 1, budget);
|
|
236
|
+
return issues.length === 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function compileTypes(value, label) {
|
|
240
|
+
if (value === undefined) return undefined;
|
|
241
|
+
const values = Array.isArray(value) ? value : [value];
|
|
242
|
+
if (values.length === 0 || !values.every((type) => typeof type === "string" && SUPPORTED_TYPES.has(type))) {
|
|
243
|
+
throw new ToolSchemaContractError(`${label}.type is invalid`);
|
|
244
|
+
}
|
|
245
|
+
if (new Set(values).size !== values.length) throw new ToolSchemaContractError(`${label}.type contains duplicates`);
|
|
246
|
+
return Object.freeze([...values]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function compileEnum(value, label) {
|
|
250
|
+
if (!Array.isArray(value) || value.length === 0) throw new ToolSchemaContractError(`${label}.enum must be a non-empty array`);
|
|
251
|
+
for (let left = 0; left < value.length; left += 1) {
|
|
252
|
+
for (let right = left + 1; right < value.length; right += 1) {
|
|
253
|
+
if (jsonEqual(value[left], value[right])) throw new ToolSchemaContractError(`${label}.enum contains duplicate values`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return structuredClone(value);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function validateDialect(value, label) {
|
|
260
|
+
if (value === undefined) return;
|
|
261
|
+
if (value !== JSON_SCHEMA_2020_12 && value !== `${JSON_SCHEMA_2020_12}#`) {
|
|
262
|
+
throw new ToolSchemaContractError(`${label} uses an unsupported JSON Schema dialect`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function matchesType(type, value) {
|
|
267
|
+
if (type === "null") return value === null;
|
|
268
|
+
if (type === "array") return Array.isArray(value);
|
|
269
|
+
if (type === "object") return isRecord(value);
|
|
270
|
+
if (type === "integer") return typeof value === "number" && Number.isInteger(value);
|
|
271
|
+
if (type === "number") return typeof value === "number" && Number.isFinite(value);
|
|
272
|
+
return typeof value === type;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function jsonEqual(left, right) {
|
|
276
|
+
if (Object.is(left, right)) return true;
|
|
277
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
278
|
+
return left.length === right.length && left.every((value, index) => jsonEqual(value, right[index]));
|
|
279
|
+
}
|
|
280
|
+
if (isRecord(left) && isRecord(right)) {
|
|
281
|
+
const leftKeys = Object.keys(left);
|
|
282
|
+
const rightKeys = Object.keys(right);
|
|
283
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.hasOwn(right, key) && jsonEqual(left[key], right[key]));
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function isMultipleOf(value, divisor) {
|
|
289
|
+
const quotient = value / divisor;
|
|
290
|
+
return Number.isFinite(quotient) && Math.abs(quotient - Math.round(quotient)) <= Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function pushIssue(issues, instancePath, keyword, message, maximumIssues) {
|
|
294
|
+
if (issues.length >= maximumIssues) return;
|
|
295
|
+
issues.push(Object.freeze({ instancePath: instancePath || "", keyword, message }));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function normalizeLimits(value) {
|
|
299
|
+
const limits = {};
|
|
300
|
+
for (const [key, fallback] of Object.entries(DEFAULT_LIMITS)) {
|
|
301
|
+
const candidate = Number(value[key]);
|
|
302
|
+
limits[key] = Number.isInteger(candidate) && candidate > 0 ? candidate : fallback;
|
|
303
|
+
}
|
|
304
|
+
return Object.freeze(limits);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function nonNegativeInteger(value, label) {
|
|
308
|
+
if (!Number.isInteger(value) || value < 0) throw new ToolSchemaContractError(`${label} must be a non-negative integer`);
|
|
309
|
+
return value;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function finiteNumber(value, label) {
|
|
313
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new ToolSchemaContractError(`${label} must be a finite number`);
|
|
314
|
+
return value;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function escapePointer(value) {
|
|
318
|
+
return String(value).replaceAll("~", "~0").replaceAll("/", "~1");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function isRecord(value) {
|
|
322
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
323
|
+
const prototype = Object.getPrototypeOf(value);
|
|
324
|
+
return prototype === Object.prototype || prototype === null;
|
|
325
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
{
|
|
3
3
|
"name": "server_info",
|
|
4
4
|
"title": "Server information",
|
|
5
|
-
"description": "Return
|
|
5
|
+
"description": "Return live authorization, effective tools, daemon/relay health, runtime state, and protocol status. Use this for authority or connectivity diagnosis, not for repository inventory. Treat authorization.effective_policy and authorization.effective_tools as authoritative; daemon.policy is only a ceiling.",
|
|
6
6
|
"availability": "always",
|
|
7
7
|
"annotations": {
|
|
8
8
|
"readOnlyHint": true,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
{
|
|
19
19
|
"name": "project_overview",
|
|
20
20
|
"title": "Project overview",
|
|
21
|
-
"description": "Summarize the connected workspace and repository. Remote responses report
|
|
21
|
+
"description": "Summarize the connected workspace, repository root, top-level entries, and project-facing runtime context. Use this for repository inventory, not as the primary live relay-health check. Remote responses report authenticated-account effective policy/tools separately from the daemon capability ceiling.",
|
|
22
22
|
"availability": "always",
|
|
23
23
|
"annotations": {
|
|
24
24
|
"readOnlyHint": true,
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
{
|
|
35
35
|
"name": "session_bootstrap",
|
|
36
36
|
"title": "Load session bootstrap",
|
|
37
|
-
"description": "Load built-in working agreements, bounded automatic project facts, user-global and root workspace instructions, and capability refresh metadata
|
|
37
|
+
"description": "Load built-in working agreements, bounded automatic project facts, user-global and root workspace instructions, and capability refresh metadata. Modern clients call it explicitly; legacy initialize clients receive the same bounded guidance through the compatibility adapter.",
|
|
38
38
|
"availability": "always",
|
|
39
39
|
"annotations": {
|
|
40
40
|
"readOnlyHint": true,
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
{
|
|
58
58
|
"name": "resolve_task_capabilities",
|
|
59
59
|
"title": "Resolve task capabilities",
|
|
60
|
-
"description": "Rescan
|
|
60
|
+
"description": "Rescan task-relevant instructions, skills, registered commands, installed applications, browser metadata, and the effective tool catalog. Return bounded set-level execution routes, ranked tools, routing ambiguity, and an optional selected skill. Pass known_refresh_fingerprint to omit unchanged static instructions while recomputing task-specific ranking. The result is advisory: direct Bash and every other policy-allowed tool remain available.",
|
|
61
61
|
"availability": "always",
|
|
62
62
|
"annotations": {
|
|
63
63
|
"readOnlyHint": true,
|
|
@@ -88,6 +88,12 @@
|
|
|
88
88
|
"include_selected_skill": {
|
|
89
89
|
"type": "boolean",
|
|
90
90
|
"default": true
|
|
91
|
+
},
|
|
92
|
+
"known_refresh_fingerprint": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"pattern": "^[a-f0-9]{64}$",
|
|
95
|
+
"maxLength": 64,
|
|
96
|
+
"description": "Previous refresh fingerprint. When it still matches, unchanged static instructions are omitted while task-specific ranking is recomputed."
|
|
91
97
|
}
|
|
92
98
|
},
|
|
93
99
|
"required": [
|
|
@@ -356,7 +362,7 @@
|
|
|
356
362
|
{
|
|
357
363
|
"name": "browser_list_tabs",
|
|
358
364
|
"title": "List browser tabs",
|
|
359
|
-
"description": "
|
|
365
|
+
"description": "Read the tab inventory from the paired existing Chromium profile without creating, activating, or closing a tab. Use browser_manage_tabs for tab mutations.",
|
|
360
366
|
"availability": "full",
|
|
361
367
|
"annotations": {
|
|
362
368
|
"readOnlyHint": true,
|
|
@@ -388,7 +394,7 @@
|
|
|
388
394
|
{
|
|
389
395
|
"name": "browser_manage_tabs",
|
|
390
396
|
"title": "Manage browser tabs",
|
|
391
|
-
"description": "Create, activate, or close tabs in the paired existing
|
|
397
|
+
"description": "Create, activate, or close tabs in the paired existing Chromium profile. Use browser_list_tabs when only a read-only tab inventory is needed.",
|
|
392
398
|
"availability": "full",
|
|
393
399
|
"annotations": {
|
|
394
400
|
"readOnlyHint": false,
|
|
@@ -437,7 +443,7 @@
|
|
|
437
443
|
{
|
|
438
444
|
"name": "browser_get_source",
|
|
439
445
|
"title": "Read browser page source",
|
|
440
|
-
"description": "Read bounded serialized
|
|
446
|
+
"description": "Read bounded raw serialized DOM HTML from the active or selected tab when source markup is required. Use browser_inspect_page for semantic elements, actionability, reusable refs, and structured interaction planning. max_bytes is one aggregate budget across at most 64 accessible frames.",
|
|
441
447
|
"availability": "full",
|
|
442
448
|
"annotations": {
|
|
443
449
|
"readOnlyHint": true,
|
|
@@ -479,7 +485,7 @@
|
|
|
479
485
|
{
|
|
480
486
|
"name": "browser_inspect_page",
|
|
481
487
|
"title": "Inspect browser page",
|
|
482
|
-
"description": "Inspect a bounded snapshot
|
|
488
|
+
"description": "Inspect a bounded semantic/actionability snapshot with reusable element refs for structured browser decisions and actions. This is not raw page source; use browser_get_source when serialized DOM HTML is required. The aggregate element budget spans at most 64 accessible frames.",
|
|
483
489
|
"availability": "full",
|
|
484
490
|
"annotations": {
|
|
485
491
|
"readOnlyHint": true,
|
|
@@ -1103,7 +1109,7 @@
|
|
|
1103
1109
|
{
|
|
1104
1110
|
"name": "agent_context",
|
|
1105
1111
|
"title": "Load agent context",
|
|
1106
|
-
"description": "Discover
|
|
1112
|
+
"description": "Discover the instruction, skill, and registered-command inventory for a target path, including precedence and provenance. Use this when the caller needs the context itself; use resolve_task_capabilities when it needs task-specific ranking and execution-route advice.",
|
|
1107
1113
|
"availability": "always",
|
|
1108
1114
|
"annotations": {
|
|
1109
1115
|
"readOnlyHint": true,
|
|
@@ -1230,7 +1236,7 @@
|
|
|
1230
1236
|
{
|
|
1231
1237
|
"name": "run_local_command",
|
|
1232
1238
|
"title": "Run registered local command",
|
|
1233
|
-
"description": "
|
|
1239
|
+
"description": "Prefer this when the repository already defines the desired operation as a registered command or package script. It runs the fixed argv/cwd/timeout contract without shell reinterpretation; use exec_command for ad hoc pipelines or run_process for an unregistered executable argv. Large output is retained for read_process.",
|
|
1234
1240
|
"availability": "direct-exec",
|
|
1235
1241
|
"annotations": {
|
|
1236
1242
|
"readOnlyHint": false,
|
|
@@ -1665,7 +1671,7 @@
|
|
|
1665
1671
|
{
|
|
1666
1672
|
"name": "run_process",
|
|
1667
1673
|
"title": "Run process directly",
|
|
1668
|
-
"description": "
|
|
1674
|
+
"description": "Run an explicit executable plus argv when no shell syntax is needed and no registered command fits. This avoids quoting, globbing, pipelines, and redirection, but it is not a sandbox; use exec_command when Bash composition is the convenient choice. Large output is retained for read_process.",
|
|
1669
1675
|
"availability": "direct-exec",
|
|
1670
1676
|
"annotations": {
|
|
1671
1677
|
"readOnlyHint": false,
|
|
@@ -2382,7 +2388,7 @@
|
|
|
2382
2388
|
{
|
|
2383
2389
|
"name": "exec_command",
|
|
2384
2390
|
"title": "Execute shell command",
|
|
2385
|
-
"description": "
|
|
2391
|
+
"description": "Run Bash-compatible shell composition in the workspace: pipelines, redirection, globbing, conditionals, or compact multi-command probes. This is the convenient general escape hatch, not a sandbox, and has the local user's operating-system authority. Prefer run_local_command for an existing fixed project command and run_process when no shell syntax is needed. Large output is retained for read_process.",
|
|
2386
2392
|
"availability": "shell-exec",
|
|
2387
2393
|
"annotations": {
|
|
2388
2394
|
"readOnlyHint": false,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { daemonLastSeenMs } from "./daemon-liveness.ts";
|
|
2
1
|
import type { DaemonSocketRegistry } from "./daemon-sockets.ts";
|
|
3
2
|
import { publicWorkerToolError, WorkerToolError } from "./errors.ts";
|
|
4
3
|
import { workerErrorClass } from "./http.ts";
|
|
@@ -9,13 +8,6 @@ import { transformDurableStreamOutcome } from "./durable-stream-result.ts";
|
|
|
9
8
|
import type { WorkerObservability } from "./observability.ts";
|
|
10
9
|
import { sendWebSocketQuietly } from "./websocket-protocol.ts";
|
|
11
10
|
|
|
12
|
-
type InvalidateDaemonSocket = (
|
|
13
|
-
socket: WebSocket,
|
|
14
|
-
message: string,
|
|
15
|
-
closeReason: string,
|
|
16
|
-
errorCode?: string,
|
|
17
|
-
) => Promise<void>;
|
|
18
|
-
|
|
19
11
|
type TransientPendingSnapshot = {
|
|
20
12
|
active: number;
|
|
21
13
|
detached: number;
|
|
@@ -29,20 +21,17 @@ export class DurableStreamCallCoordinator {
|
|
|
29
21
|
private readonly daemonRegistry: DaemonSocketRegistry;
|
|
30
22
|
private readonly observability: WorkerObservability;
|
|
31
23
|
private readonly maximumPendingCalls: number;
|
|
32
|
-
private readonly invalidateDaemonSocket: InvalidateDaemonSocket;
|
|
33
24
|
|
|
34
25
|
constructor(
|
|
35
26
|
calls: McpPendingCallStore,
|
|
36
27
|
daemonRegistry: DaemonSocketRegistry,
|
|
37
28
|
observability: WorkerObservability,
|
|
38
29
|
maximumPendingCalls: number,
|
|
39
|
-
invalidateDaemonSocket: InvalidateDaemonSocket,
|
|
40
30
|
) {
|
|
41
31
|
this.calls = calls;
|
|
42
32
|
this.daemonRegistry = daemonRegistry;
|
|
43
33
|
this.observability = observability;
|
|
44
34
|
this.maximumPendingCalls = maximumPendingCalls;
|
|
45
|
-
this.invalidateDaemonSocket = invalidateDaemonSocket;
|
|
46
35
|
}
|
|
47
36
|
|
|
48
37
|
async snapshot(transient: TransientPendingSnapshot): Promise<Record<string, unknown>> {
|
|
@@ -66,9 +55,10 @@ export class DurableStreamCallCoordinator {
|
|
|
66
55
|
connectionId: string,
|
|
67
56
|
outcome: PendingCallOutcome,
|
|
68
57
|
knownCall?: PendingStreamCallView,
|
|
69
|
-
): Promise<
|
|
58
|
+
): Promise<"committed" | "missing" | "stale"> {
|
|
70
59
|
const call = knownCall ?? await this.calls.get(callId);
|
|
71
|
-
if (!call
|
|
60
|
+
if (!call) return "missing";
|
|
61
|
+
if (call.connection_id !== connectionId) return "stale";
|
|
72
62
|
const normalized = transformDurableStreamOutcome(call, outcome);
|
|
73
63
|
const code = normalized.ok ? "" : publicWorkerToolError(normalized.error).code;
|
|
74
64
|
try {
|
|
@@ -77,23 +67,21 @@ export class DurableStreamCallCoordinator {
|
|
|
77
67
|
connectionId,
|
|
78
68
|
streamTerminalMessage(call.requestId, normalized),
|
|
79
69
|
);
|
|
80
|
-
if (!completed)
|
|
70
|
+
if (!completed) {
|
|
71
|
+
const current = await this.calls.get(callId);
|
|
72
|
+
return current ? "stale" : "missing";
|
|
73
|
+
}
|
|
81
74
|
} catch (error) {
|
|
82
75
|
this.observability.event("error", "mcp.stream.persist.failed", { error_class: workerErrorClass(error) });
|
|
76
|
+
throw error;
|
|
83
77
|
}
|
|
84
78
|
this.observability.callFinished(call.tool, code);
|
|
85
|
-
return
|
|
79
|
+
return "committed";
|
|
86
80
|
}
|
|
87
81
|
|
|
88
82
|
async expire(call: PendingStreamCallView): Promise<void> {
|
|
89
83
|
const socket = call.state === "attached" ? this.daemonRegistry.socketForConnectionId(call.connection_id) : undefined;
|
|
90
|
-
if (socket) {
|
|
91
|
-
sendWebSocketQuietly(socket, { type: "cancel_call", id: call.call_id });
|
|
92
|
-
const silentForMs = Date.now() - daemonLastSeenMs(this.daemonRegistry.readyAttachment(socket));
|
|
93
|
-
if (!Number.isFinite(silentForMs) || silentForMs > 45_000) {
|
|
94
|
-
void this.invalidateDaemonSocket(socket, "daemon became unresponsive", "daemon liveness timeout");
|
|
95
|
-
}
|
|
96
|
-
}
|
|
84
|
+
if (socket) sendWebSocketQuietly(socket, { type: "cancel_call", id: call.call_id });
|
|
97
85
|
const error = call.state === "attached"
|
|
98
86
|
? new WorkerToolError("timeout", `daemon tool timed out: ${call.tool}`, true)
|
|
99
87
|
: new WorkerToolError("unavailable", "daemon disconnected; reconnect grace expired", true);
|
|
@@ -111,12 +99,12 @@ export class DurableStreamCallCoordinator {
|
|
|
111
99
|
if (!call) return false;
|
|
112
100
|
const socket = call.state === "attached" ? this.daemonRegistry.socketForConnectionId(call.connection_id) : undefined;
|
|
113
101
|
if (socket) sendWebSocketQuietly(socket, { type: "cancel_call", id: call.call_id });
|
|
114
|
-
return this.settle(
|
|
102
|
+
return (await this.settle(
|
|
115
103
|
call.call_id,
|
|
116
104
|
call.connection_id,
|
|
117
105
|
{ ok: false, error: new WorkerToolError("cancelled", "tool call cancelled by client") },
|
|
118
106
|
call,
|
|
119
|
-
);
|
|
107
|
+
)) === "committed";
|
|
120
108
|
}
|
|
121
109
|
|
|
122
110
|
detach(connectionId: string, graceMs: number): Promise<number> {
|
package/src/worker/http.ts
CHANGED
|
@@ -235,17 +235,30 @@ export function normalizeRedirectUri(value: string): string | null {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
export function
|
|
238
|
+
export function mcpOriginRejection(request: Request, base: string, configured: string): Response | null {
|
|
239
|
+
const origin = request.headers.get("Origin") ?? "";
|
|
240
|
+
if (!origin || isConfiguredOrSameOrigin(origin, base, configured)) return null;
|
|
241
|
+
return json({ error: "origin_not_allowed" }, 403);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function corsPreflight(
|
|
245
|
+
request: Request, base: string, configured: string, allowedParameterHeaders: ReadonlySet<string> = EMPTY_CORS_HEADER_SET,
|
|
246
|
+
): Response {
|
|
239
247
|
const origin = request.headers.get("Origin") ?? "";
|
|
240
248
|
if (!isConfiguredOrSameOrigin(origin, base, configured)) return json({ error: "origin_not_allowed" }, 403);
|
|
241
249
|
const requestedMethod = (request.headers.get("Access-Control-Request-Method") ?? "").toUpperCase();
|
|
242
250
|
if (requestedMethod && !["GET", "POST"].includes(requestedMethod)) return methodNotAllowed("GET, POST, OPTIONS");
|
|
251
|
+
const requestedHeaders = requestedCorsHeaders(request);
|
|
252
|
+
if (!requestedHeaders) return json({ error: "cors_header_invalid" }, 400);
|
|
253
|
+
const rejected = requestedHeaders.filter((name) => !isAllowedMcpCorsHeader(name, allowedParameterHeaders));
|
|
254
|
+
if (rejected.length > 0) return json({ error: "cors_header_not_allowed" }, 403);
|
|
255
|
+
const allowedHeaders = [...new Set([...DEFAULT_MCP_CORS_HEADERS, ...requestedHeaders])];
|
|
243
256
|
return new Response(null, {
|
|
244
257
|
status: 204,
|
|
245
258
|
headers: {
|
|
246
259
|
"access-control-allow-origin": origin,
|
|
247
260
|
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
248
|
-
"access-control-allow-headers": "
|
|
261
|
+
"access-control-allow-headers": allowedHeaders.join(", "),
|
|
249
262
|
"access-control-max-age": "600",
|
|
250
263
|
"cache-control": "no-store",
|
|
251
264
|
"vary": "Origin, Access-Control-Request-Method, Access-Control-Request-Headers",
|
|
@@ -253,6 +266,27 @@ export function corsPreflight(request: Request, base: string, configured: string
|
|
|
253
266
|
});
|
|
254
267
|
}
|
|
255
268
|
|
|
269
|
+
const DEFAULT_MCP_CORS_HEADERS = Object.freeze([
|
|
270
|
+
"authorization", "content-type", "dpop", "mcp-protocol-version", "mcp-method", "mcp-name",
|
|
271
|
+
"mcp-session-id", "last-event-id", "traceparent", "tracestate", "baggage",
|
|
272
|
+
]);
|
|
273
|
+
const EMPTY_CORS_HEADER_SET: ReadonlySet<string> = new Set();
|
|
274
|
+
const CORS_HEADER_NAME = /^[!#$%&'*+.^_`|~0-9a-z-]+$/;
|
|
275
|
+
const MAX_CORS_REQUEST_HEADER_BYTES = 8192;
|
|
276
|
+
const MAX_CORS_REQUEST_HEADERS = 64;
|
|
277
|
+
|
|
278
|
+
function requestedCorsHeaders(request: Request): string[] | null {
|
|
279
|
+
const raw = request.headers.get("Access-Control-Request-Headers") ?? "";
|
|
280
|
+
if (new TextEncoder().encode(raw).byteLength > MAX_CORS_REQUEST_HEADER_BYTES) return null;
|
|
281
|
+
const values = [...new Set(raw.split(",").map((value) => value.trim().toLowerCase()).filter(Boolean))];
|
|
282
|
+
if (values.length > MAX_CORS_REQUEST_HEADERS || values.some((name) => !CORS_HEADER_NAME.test(name))) return null;
|
|
283
|
+
return values;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function isAllowedMcpCorsHeader(name: string, allowedParameterHeaders: ReadonlySet<string>): boolean {
|
|
287
|
+
return DEFAULT_MCP_CORS_HEADERS.includes(name) || allowedParameterHeaders.has(name);
|
|
288
|
+
}
|
|
289
|
+
|
|
256
290
|
export function applyCors(response: Response, request: Request, base: string, configured: string): Response {
|
|
257
291
|
if (response.status === 101) return response;
|
|
258
292
|
const origin = request.headers.get("Origin") ?? "";
|