@valbuild/next 0.115.0 → 0.116.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/declarations/src/server/index.d.ts +2 -0
- package/dist/declarations/src/server/initValMcp.d.ts +42 -0
- package/package.json +6 -6
- package/server/dist/valbuild-next-server.cjs.dev.js +258 -0
- package/server/dist/valbuild-next-server.cjs.prod.js +258 -0
- package/server/dist/valbuild-next-server.esm.js +259 -2
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ValConfig, ValModules } from "@valbuild/core";
|
|
2
|
+
import { type ValToolContext, type ValTools } from "@valbuild/server";
|
|
3
|
+
/**
|
|
4
|
+
* Val's tools over MCP, and the two checks that have to happen before a request
|
|
5
|
+
* gets to them.
|
|
6
|
+
*
|
|
7
|
+
* Nothing here imports an MCP SDK. The app owns the transport — which SDK, which
|
|
8
|
+
* route, which framework — and this owns the parts that must not be re-decided
|
|
9
|
+
* per app: whether the request is allowed to reach the tools at all, and whose
|
|
10
|
+
* credential it carries. `docs/plans/mcp.md` Part A has the reasoning; the short
|
|
11
|
+
* version is that the SDK reorganised itself once already, and the security
|
|
12
|
+
* checks should not move when it does again.
|
|
13
|
+
*/
|
|
14
|
+
export type ValMcpAuthorizationResult = {
|
|
15
|
+
status: "ok";
|
|
16
|
+
tools: ValTools;
|
|
17
|
+
ctx: ValToolContext;
|
|
18
|
+
} | {
|
|
19
|
+
status: "refused";
|
|
20
|
+
response: Response;
|
|
21
|
+
};
|
|
22
|
+
export type ValMcp = {
|
|
23
|
+
/**
|
|
24
|
+
* Check a request and, if it is allowed, hand back the tools and the context
|
|
25
|
+
* to call them with.
|
|
26
|
+
*
|
|
27
|
+
* Call this per request — both halves are per request. Refusing early is the
|
|
28
|
+
* point: a refused request must not reach the protocol layer, let alone a
|
|
29
|
+
* tool.
|
|
30
|
+
*/
|
|
31
|
+
valMcpAuthorize: (request: Request | undefined) => Promise<ValMcpAuthorizationResult>;
|
|
32
|
+
/**
|
|
33
|
+
* The registry itself, for listing tools at startup.
|
|
34
|
+
*
|
|
35
|
+
* Listing needs no credential — it reads no content — so registering tools
|
|
36
|
+
* with an MCP server can happen once rather than per request.
|
|
37
|
+
*/
|
|
38
|
+
valMcpTools: () => Promise<ValTools>;
|
|
39
|
+
};
|
|
40
|
+
export declare function initValMcp(valModules: ValModules, config: ValConfig, opts?: {
|
|
41
|
+
formatter?: (code: string, filePath: string) => string | Promise<string>;
|
|
42
|
+
}): ValMcp;
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"next",
|
|
13
13
|
"react"
|
|
14
14
|
],
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.116.0",
|
|
16
16
|
"main": "dist/valbuild-next.cjs.js",
|
|
17
17
|
"module": "dist/valbuild-next.esm.js",
|
|
18
18
|
"exports": {
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"client-only": "^0.0.1",
|
|
49
49
|
"server-only": "^0.0.1",
|
|
50
50
|
"@valbuild/core": "0.111.0",
|
|
51
|
-
"@valbuild/language-server": "0.
|
|
52
|
-
"@valbuild/
|
|
53
|
-
"@valbuild/
|
|
54
|
-
"@valbuild/
|
|
55
|
-
"@valbuild/ui": "0.
|
|
51
|
+
"@valbuild/language-server": "0.116.0",
|
|
52
|
+
"@valbuild/react": "0.116.0",
|
|
53
|
+
"@valbuild/server": "0.116.0",
|
|
54
|
+
"@valbuild/shared": "0.116.0",
|
|
55
|
+
"@valbuild/ui": "0.116.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@testing-library/react": "^16.3.3",
|
|
@@ -120,4 +120,262 @@ function initValServer(valModules, config, nextConfig) {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Val's tools over MCP, and the two checks that have to happen before a request
|
|
125
|
+
* gets to them.
|
|
126
|
+
*
|
|
127
|
+
* Nothing here imports an MCP SDK. The app owns the transport — which SDK, which
|
|
128
|
+
* route, which framework — and this owns the parts that must not be re-decided
|
|
129
|
+
* per app: whether the request is allowed to reach the tools at all, and whose
|
|
130
|
+
* credential it carries. `docs/plans/mcp.md` Part A has the reasoning; the short
|
|
131
|
+
* version is that the SDK reorganised itself once already, and the security
|
|
132
|
+
* checks should not move when it does again.
|
|
133
|
+
*/
|
|
134
|
+
|
|
135
|
+
function initValMcp(valModules, config, opts) {
|
|
136
|
+
var route = "/api/val"; // TODO: get from config, as initValServer does
|
|
137
|
+
var coreVersion = core.Internal.VERSION.core;
|
|
138
|
+
if (!coreVersion) {
|
|
139
|
+
throw new Error("Could not get @valbuild/core package version");
|
|
140
|
+
}
|
|
141
|
+
var nextVersion = version.VERSION;
|
|
142
|
+
if (!nextVersion) {
|
|
143
|
+
throw new Error("Could not get @valbuild/next package version");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Resolved once at module-eval time, awaited per request. The no-op catch is
|
|
147
|
+
// load bearing for the same reason it is in createValApiRouter: a config error
|
|
148
|
+
// on a promise with no handler attached becomes an unhandledRejection and
|
|
149
|
+
// takes the dev server down, and the error is reported per request below
|
|
150
|
+
// anyway.
|
|
151
|
+
var setupPromise = version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee() {
|
|
152
|
+
var options;
|
|
153
|
+
return version._regenerator().w(function (_context) {
|
|
154
|
+
while (1) switch (_context.n) {
|
|
155
|
+
case 0:
|
|
156
|
+
_context.n = 1;
|
|
157
|
+
return server.initHandlerOptions(route, objectSpread2._objectSpread2(objectSpread2._objectSpread2({}, config), {}, {
|
|
158
|
+
versions: {
|
|
159
|
+
core: coreVersion,
|
|
160
|
+
next: nextVersion
|
|
161
|
+
}
|
|
162
|
+
}), config);
|
|
163
|
+
case 1:
|
|
164
|
+
options = _context.v;
|
|
165
|
+
return _context.a(2, {
|
|
166
|
+
mode: options.mode,
|
|
167
|
+
tools: server.createValTools(valModules, objectSpread2._objectSpread2(objectSpread2._objectSpread2({}, options), {}, {
|
|
168
|
+
formatter: opts === null || opts === void 0 ? void 0 : opts.formatter
|
|
169
|
+
}))
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}, _callee);
|
|
173
|
+
}))();
|
|
174
|
+
setupPromise["catch"](function () {
|
|
175
|
+
// handled per request
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
valMcpTools: function valMcpTools() {
|
|
179
|
+
return version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee2() {
|
|
180
|
+
return version._regenerator().w(function (_context2) {
|
|
181
|
+
while (1) switch (_context2.n) {
|
|
182
|
+
case 0:
|
|
183
|
+
_context2.n = 1;
|
|
184
|
+
return setupPromise;
|
|
185
|
+
case 1:
|
|
186
|
+
return _context2.a(2, _context2.v.tools);
|
|
187
|
+
}
|
|
188
|
+
}, _callee2);
|
|
189
|
+
}))();
|
|
190
|
+
},
|
|
191
|
+
valMcpAuthorize: function valMcpAuthorize(request) {
|
|
192
|
+
return version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee3() {
|
|
193
|
+
var setup, refusal, pat, _t;
|
|
194
|
+
return version._regenerator().w(function (_context3) {
|
|
195
|
+
while (1) switch (_context3.p = _context3.n) {
|
|
196
|
+
case 0:
|
|
197
|
+
_context3.p = 0;
|
|
198
|
+
_context3.n = 1;
|
|
199
|
+
return setupPromise;
|
|
200
|
+
case 1:
|
|
201
|
+
setup = _context3.v;
|
|
202
|
+
_context3.n = 3;
|
|
203
|
+
break;
|
|
204
|
+
case 2:
|
|
205
|
+
_context3.p = 2;
|
|
206
|
+
_t = _context3.v;
|
|
207
|
+
return _context3.a(2, {
|
|
208
|
+
status: "refused",
|
|
209
|
+
response: jsonResponse(500, {
|
|
210
|
+
error: "Val: could not start the Val MCP server",
|
|
211
|
+
details: _t instanceof Error ? _t.message : String(_t)
|
|
212
|
+
})
|
|
213
|
+
});
|
|
214
|
+
case 3:
|
|
215
|
+
if (!(request === undefined)) {
|
|
216
|
+
_context3.n = 4;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
return _context3.a(2, {
|
|
220
|
+
status: "refused",
|
|
221
|
+
response: jsonResponse(401, {
|
|
222
|
+
error: "Val: this MCP server needs the HTTP request to authorize a call, and none was available."
|
|
223
|
+
})
|
|
224
|
+
});
|
|
225
|
+
case 4:
|
|
226
|
+
refusal = refuseUnsafeRequest(request, setup.mode);
|
|
227
|
+
if (!refusal) {
|
|
228
|
+
_context3.n = 5;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
return _context3.a(2, {
|
|
232
|
+
status: "refused",
|
|
233
|
+
response: refusal
|
|
234
|
+
});
|
|
235
|
+
case 5:
|
|
236
|
+
pat = readBearerToken(request);
|
|
237
|
+
return _context3.a(2, {
|
|
238
|
+
status: "ok",
|
|
239
|
+
tools: setup.tools,
|
|
240
|
+
ctx: {
|
|
241
|
+
// Passed through unverified, deliberately: this app is not the
|
|
242
|
+
// authority on what a token may do, and the registry sends it to the
|
|
243
|
+
// backend that is. See `docs/plans/mcp.md` D.2.
|
|
244
|
+
auth: pat === null ? null : {
|
|
245
|
+
pat: pat
|
|
246
|
+
},
|
|
247
|
+
// Not the MCP session id. Val's patch `sessionId` names a Val AI
|
|
248
|
+
// session, and putting an unrelated id in it would claim a
|
|
249
|
+
// relationship that does not exist.
|
|
250
|
+
sessionId: null
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}, _callee3, null, [[0, 2]]);
|
|
255
|
+
}))();
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The two ways this route is dangerous, both refused here.
|
|
262
|
+
*
|
|
263
|
+
* 1. **Local filesystem mode outside development.** In fs mode there is no
|
|
264
|
+
* credential and no backend: the tools read and write the running process's
|
|
265
|
+
* own working tree, and every permission check Val has lives on the other
|
|
266
|
+
* side of a backend that is not in this path. Exposed on a deployed host,
|
|
267
|
+
* that is unauthenticated write access to the site's content for anyone who
|
|
268
|
+
* can reach the port. There is no configuration that makes it safe, so there
|
|
269
|
+
* is no flag to turn this off — a project that wants MCP in production wants
|
|
270
|
+
* proxy mode, where every call carries its caller's own token.
|
|
271
|
+
*
|
|
272
|
+
* 2. **A browser driving the local server.** A page on any origin can `fetch`
|
|
273
|
+
* `http://localhost:3000/api/mcp` while a developer has the app running, and
|
|
274
|
+
* with DNS rebinding it can do so with a `Host` of its own choosing. Neither
|
|
275
|
+
* needs a credential in fs mode. So a cross-origin `Origin` is refused, and
|
|
276
|
+
* in fs mode the request must actually be addressed to a loopback host.
|
|
277
|
+
*
|
|
278
|
+
* MCP clients are not browsers and send no `Origin`, so the check costs them
|
|
279
|
+
* nothing.
|
|
280
|
+
*/
|
|
281
|
+
function refuseUnsafeRequest(request, mode) {
|
|
282
|
+
if (mode === "fs" && process.env.NODE_ENV !== "development") {
|
|
283
|
+
return jsonResponse(403, {
|
|
284
|
+
error: "Val: the MCP endpoint is disabled. This project is running in local filesystem mode, where MCP calls are unauthenticated and write directly to the working tree, so it is only served in development. Configure Val for proxy mode to use MCP on a deployed host."
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
var host = requestHost(request);
|
|
288
|
+
var origin = request.headers.get("origin");
|
|
289
|
+
if (origin !== null) {
|
|
290
|
+
// `Origin: null` is refused along with the rest. It is the *opaque* origin —
|
|
291
|
+
// a sandboxed iframe, a `file://` page, some redirects — so it cannot be
|
|
292
|
+
// compared to anything, and "cannot be compared" has to mean refuse: a page
|
|
293
|
+
// that would fail the check can otherwise pass it by arranging to have no
|
|
294
|
+
// origin at all. Absent entirely is the case that is allowed, and that is
|
|
295
|
+
// the one MCP clients produce.
|
|
296
|
+
var originHost = origin === "null" ? null : hostOf(origin);
|
|
297
|
+
if (originHost === null || host === null || originHost !== host) {
|
|
298
|
+
return jsonResponse(403, {
|
|
299
|
+
error: "Val: refusing a cross-origin MCP request from ".concat(JSON.stringify(origin), ". MCP clients do not send an Origin header; a browser does.")
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (mode === "fs") {
|
|
304
|
+
var hostname = host === null ? null : hostnameOf(host);
|
|
305
|
+
if (hostname === null || !LOOPBACK_HOSTNAMES.has(hostname)) {
|
|
306
|
+
return jsonResponse(403, {
|
|
307
|
+
error: "Val: refusing an MCP request addressed to ".concat(JSON.stringify(host !== null && host !== void 0 ? host : "an unknown host"), ". In local filesystem mode this endpoint only answers on localhost, because a name that resolves to 127.0.0.1 is how a web page reaches a developer's own machine.")
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
var LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Which host the request was addressed to, as `hostname:port`.
|
|
317
|
+
*
|
|
318
|
+
* `Host` only, and deliberately **not** `X-Forwarded-Host`. The forwarded header
|
|
319
|
+
* is what a client asked for behind a proxy, but nothing stops a client sending
|
|
320
|
+
* it directly — so preferring it hands an attacker the value both checks below
|
|
321
|
+
* are decided on. `Host`, by contrast, a browser sets from the URL and page
|
|
322
|
+
* script cannot override, which is exactly the property the loopback check
|
|
323
|
+
* depends on.
|
|
324
|
+
*
|
|
325
|
+
* The cost is that behind a proxy that rewrites `Host`, a *browser* request
|
|
326
|
+
* whose `Origin` is the public name no longer matches. That is acceptable: such
|
|
327
|
+
* a request carries no personal access token, so proxy mode refuses it anyway,
|
|
328
|
+
* and a non-browser MCP client sends no `Origin` and never reaches the
|
|
329
|
+
* comparison. Trusting the forwarded header would need an explicit trusted-proxy
|
|
330
|
+
* configuration, which is a bigger thing than this needs.
|
|
331
|
+
*/
|
|
332
|
+
function requestHost(request) {
|
|
333
|
+
var host = request.headers.get("host");
|
|
334
|
+
if (host) {
|
|
335
|
+
return host.trim().toLowerCase();
|
|
336
|
+
}
|
|
337
|
+
// Last resort: the URL the framework saw.
|
|
338
|
+
try {
|
|
339
|
+
return new URL(request.url).host.toLowerCase();
|
|
340
|
+
} catch (_unused) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function hostOf(origin) {
|
|
345
|
+
try {
|
|
346
|
+
return new URL(origin).host.toLowerCase();
|
|
347
|
+
} catch (_unused2) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Strips the port, keeping IPv6 brackets — `[::1]:3000` is hostname `[::1]`. */
|
|
353
|
+
function hostnameOf(host) {
|
|
354
|
+
if (host.startsWith("[")) {
|
|
355
|
+
var end = host.indexOf("]");
|
|
356
|
+
return end === -1 ? host : host.slice(0, end + 1);
|
|
357
|
+
}
|
|
358
|
+
var colon = host.indexOf(":");
|
|
359
|
+
return colon === -1 ? host : host.slice(0, colon);
|
|
360
|
+
}
|
|
361
|
+
function readBearerToken(request) {
|
|
362
|
+
var _match$;
|
|
363
|
+
var header = request.headers.get("authorization");
|
|
364
|
+
if (!header) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
var match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
368
|
+
var token = match === null || match === void 0 || (_match$ = match[1]) === null || _match$ === void 0 ? void 0 : _match$.trim();
|
|
369
|
+
return token ? token : null;
|
|
370
|
+
}
|
|
371
|
+
function jsonResponse(status, body) {
|
|
372
|
+
return new Response(JSON.stringify(body), {
|
|
373
|
+
status: status,
|
|
374
|
+
headers: {
|
|
375
|
+
"Content-Type": "application/json"
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
exports.initValMcp = initValMcp;
|
|
123
381
|
exports.initValServer = initValServer;
|
|
@@ -120,4 +120,262 @@ function initValServer(valModules, config, nextConfig) {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Val's tools over MCP, and the two checks that have to happen before a request
|
|
125
|
+
* gets to them.
|
|
126
|
+
*
|
|
127
|
+
* Nothing here imports an MCP SDK. The app owns the transport — which SDK, which
|
|
128
|
+
* route, which framework — and this owns the parts that must not be re-decided
|
|
129
|
+
* per app: whether the request is allowed to reach the tools at all, and whose
|
|
130
|
+
* credential it carries. `docs/plans/mcp.md` Part A has the reasoning; the short
|
|
131
|
+
* version is that the SDK reorganised itself once already, and the security
|
|
132
|
+
* checks should not move when it does again.
|
|
133
|
+
*/
|
|
134
|
+
|
|
135
|
+
function initValMcp(valModules, config, opts) {
|
|
136
|
+
var route = "/api/val"; // TODO: get from config, as initValServer does
|
|
137
|
+
var coreVersion = core.Internal.VERSION.core;
|
|
138
|
+
if (!coreVersion) {
|
|
139
|
+
throw new Error("Could not get @valbuild/core package version");
|
|
140
|
+
}
|
|
141
|
+
var nextVersion = version.VERSION;
|
|
142
|
+
if (!nextVersion) {
|
|
143
|
+
throw new Error("Could not get @valbuild/next package version");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Resolved once at module-eval time, awaited per request. The no-op catch is
|
|
147
|
+
// load bearing for the same reason it is in createValApiRouter: a config error
|
|
148
|
+
// on a promise with no handler attached becomes an unhandledRejection and
|
|
149
|
+
// takes the dev server down, and the error is reported per request below
|
|
150
|
+
// anyway.
|
|
151
|
+
var setupPromise = version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee() {
|
|
152
|
+
var options;
|
|
153
|
+
return version._regenerator().w(function (_context) {
|
|
154
|
+
while (1) switch (_context.n) {
|
|
155
|
+
case 0:
|
|
156
|
+
_context.n = 1;
|
|
157
|
+
return server.initHandlerOptions(route, objectSpread2._objectSpread2(objectSpread2._objectSpread2({}, config), {}, {
|
|
158
|
+
versions: {
|
|
159
|
+
core: coreVersion,
|
|
160
|
+
next: nextVersion
|
|
161
|
+
}
|
|
162
|
+
}), config);
|
|
163
|
+
case 1:
|
|
164
|
+
options = _context.v;
|
|
165
|
+
return _context.a(2, {
|
|
166
|
+
mode: options.mode,
|
|
167
|
+
tools: server.createValTools(valModules, objectSpread2._objectSpread2(objectSpread2._objectSpread2({}, options), {}, {
|
|
168
|
+
formatter: opts === null || opts === void 0 ? void 0 : opts.formatter
|
|
169
|
+
}))
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}, _callee);
|
|
173
|
+
}))();
|
|
174
|
+
setupPromise["catch"](function () {
|
|
175
|
+
// handled per request
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
valMcpTools: function valMcpTools() {
|
|
179
|
+
return version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee2() {
|
|
180
|
+
return version._regenerator().w(function (_context2) {
|
|
181
|
+
while (1) switch (_context2.n) {
|
|
182
|
+
case 0:
|
|
183
|
+
_context2.n = 1;
|
|
184
|
+
return setupPromise;
|
|
185
|
+
case 1:
|
|
186
|
+
return _context2.a(2, _context2.v.tools);
|
|
187
|
+
}
|
|
188
|
+
}, _callee2);
|
|
189
|
+
}))();
|
|
190
|
+
},
|
|
191
|
+
valMcpAuthorize: function valMcpAuthorize(request) {
|
|
192
|
+
return version._asyncToGenerator(/*#__PURE__*/version._regenerator().m(function _callee3() {
|
|
193
|
+
var setup, refusal, pat, _t;
|
|
194
|
+
return version._regenerator().w(function (_context3) {
|
|
195
|
+
while (1) switch (_context3.p = _context3.n) {
|
|
196
|
+
case 0:
|
|
197
|
+
_context3.p = 0;
|
|
198
|
+
_context3.n = 1;
|
|
199
|
+
return setupPromise;
|
|
200
|
+
case 1:
|
|
201
|
+
setup = _context3.v;
|
|
202
|
+
_context3.n = 3;
|
|
203
|
+
break;
|
|
204
|
+
case 2:
|
|
205
|
+
_context3.p = 2;
|
|
206
|
+
_t = _context3.v;
|
|
207
|
+
return _context3.a(2, {
|
|
208
|
+
status: "refused",
|
|
209
|
+
response: jsonResponse(500, {
|
|
210
|
+
error: "Val: could not start the Val MCP server",
|
|
211
|
+
details: _t instanceof Error ? _t.message : String(_t)
|
|
212
|
+
})
|
|
213
|
+
});
|
|
214
|
+
case 3:
|
|
215
|
+
if (!(request === undefined)) {
|
|
216
|
+
_context3.n = 4;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
return _context3.a(2, {
|
|
220
|
+
status: "refused",
|
|
221
|
+
response: jsonResponse(401, {
|
|
222
|
+
error: "Val: this MCP server needs the HTTP request to authorize a call, and none was available."
|
|
223
|
+
})
|
|
224
|
+
});
|
|
225
|
+
case 4:
|
|
226
|
+
refusal = refuseUnsafeRequest(request, setup.mode);
|
|
227
|
+
if (!refusal) {
|
|
228
|
+
_context3.n = 5;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
return _context3.a(2, {
|
|
232
|
+
status: "refused",
|
|
233
|
+
response: refusal
|
|
234
|
+
});
|
|
235
|
+
case 5:
|
|
236
|
+
pat = readBearerToken(request);
|
|
237
|
+
return _context3.a(2, {
|
|
238
|
+
status: "ok",
|
|
239
|
+
tools: setup.tools,
|
|
240
|
+
ctx: {
|
|
241
|
+
// Passed through unverified, deliberately: this app is not the
|
|
242
|
+
// authority on what a token may do, and the registry sends it to the
|
|
243
|
+
// backend that is. See `docs/plans/mcp.md` D.2.
|
|
244
|
+
auth: pat === null ? null : {
|
|
245
|
+
pat: pat
|
|
246
|
+
},
|
|
247
|
+
// Not the MCP session id. Val's patch `sessionId` names a Val AI
|
|
248
|
+
// session, and putting an unrelated id in it would claim a
|
|
249
|
+
// relationship that does not exist.
|
|
250
|
+
sessionId: null
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}, _callee3, null, [[0, 2]]);
|
|
255
|
+
}))();
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The two ways this route is dangerous, both refused here.
|
|
262
|
+
*
|
|
263
|
+
* 1. **Local filesystem mode outside development.** In fs mode there is no
|
|
264
|
+
* credential and no backend: the tools read and write the running process's
|
|
265
|
+
* own working tree, and every permission check Val has lives on the other
|
|
266
|
+
* side of a backend that is not in this path. Exposed on a deployed host,
|
|
267
|
+
* that is unauthenticated write access to the site's content for anyone who
|
|
268
|
+
* can reach the port. There is no configuration that makes it safe, so there
|
|
269
|
+
* is no flag to turn this off — a project that wants MCP in production wants
|
|
270
|
+
* proxy mode, where every call carries its caller's own token.
|
|
271
|
+
*
|
|
272
|
+
* 2. **A browser driving the local server.** A page on any origin can `fetch`
|
|
273
|
+
* `http://localhost:3000/api/mcp` while a developer has the app running, and
|
|
274
|
+
* with DNS rebinding it can do so with a `Host` of its own choosing. Neither
|
|
275
|
+
* needs a credential in fs mode. So a cross-origin `Origin` is refused, and
|
|
276
|
+
* in fs mode the request must actually be addressed to a loopback host.
|
|
277
|
+
*
|
|
278
|
+
* MCP clients are not browsers and send no `Origin`, so the check costs them
|
|
279
|
+
* nothing.
|
|
280
|
+
*/
|
|
281
|
+
function refuseUnsafeRequest(request, mode) {
|
|
282
|
+
if (mode === "fs" && "production" !== "development") {
|
|
283
|
+
return jsonResponse(403, {
|
|
284
|
+
error: "Val: the MCP endpoint is disabled. This project is running in local filesystem mode, where MCP calls are unauthenticated and write directly to the working tree, so it is only served in development. Configure Val for proxy mode to use MCP on a deployed host."
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
var host = requestHost(request);
|
|
288
|
+
var origin = request.headers.get("origin");
|
|
289
|
+
if (origin !== null) {
|
|
290
|
+
// `Origin: null` is refused along with the rest. It is the *opaque* origin —
|
|
291
|
+
// a sandboxed iframe, a `file://` page, some redirects — so it cannot be
|
|
292
|
+
// compared to anything, and "cannot be compared" has to mean refuse: a page
|
|
293
|
+
// that would fail the check can otherwise pass it by arranging to have no
|
|
294
|
+
// origin at all. Absent entirely is the case that is allowed, and that is
|
|
295
|
+
// the one MCP clients produce.
|
|
296
|
+
var originHost = origin === "null" ? null : hostOf(origin);
|
|
297
|
+
if (originHost === null || host === null || originHost !== host) {
|
|
298
|
+
return jsonResponse(403, {
|
|
299
|
+
error: "Val: refusing a cross-origin MCP request from ".concat(JSON.stringify(origin), ". MCP clients do not send an Origin header; a browser does.")
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (mode === "fs") {
|
|
304
|
+
var hostname = host === null ? null : hostnameOf(host);
|
|
305
|
+
if (hostname === null || !LOOPBACK_HOSTNAMES.has(hostname)) {
|
|
306
|
+
return jsonResponse(403, {
|
|
307
|
+
error: "Val: refusing an MCP request addressed to ".concat(JSON.stringify(host !== null && host !== void 0 ? host : "an unknown host"), ". In local filesystem mode this endpoint only answers on localhost, because a name that resolves to 127.0.0.1 is how a web page reaches a developer's own machine.")
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
var LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Which host the request was addressed to, as `hostname:port`.
|
|
317
|
+
*
|
|
318
|
+
* `Host` only, and deliberately **not** `X-Forwarded-Host`. The forwarded header
|
|
319
|
+
* is what a client asked for behind a proxy, but nothing stops a client sending
|
|
320
|
+
* it directly — so preferring it hands an attacker the value both checks below
|
|
321
|
+
* are decided on. `Host`, by contrast, a browser sets from the URL and page
|
|
322
|
+
* script cannot override, which is exactly the property the loopback check
|
|
323
|
+
* depends on.
|
|
324
|
+
*
|
|
325
|
+
* The cost is that behind a proxy that rewrites `Host`, a *browser* request
|
|
326
|
+
* whose `Origin` is the public name no longer matches. That is acceptable: such
|
|
327
|
+
* a request carries no personal access token, so proxy mode refuses it anyway,
|
|
328
|
+
* and a non-browser MCP client sends no `Origin` and never reaches the
|
|
329
|
+
* comparison. Trusting the forwarded header would need an explicit trusted-proxy
|
|
330
|
+
* configuration, which is a bigger thing than this needs.
|
|
331
|
+
*/
|
|
332
|
+
function requestHost(request) {
|
|
333
|
+
var host = request.headers.get("host");
|
|
334
|
+
if (host) {
|
|
335
|
+
return host.trim().toLowerCase();
|
|
336
|
+
}
|
|
337
|
+
// Last resort: the URL the framework saw.
|
|
338
|
+
try {
|
|
339
|
+
return new URL(request.url).host.toLowerCase();
|
|
340
|
+
} catch (_unused) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function hostOf(origin) {
|
|
345
|
+
try {
|
|
346
|
+
return new URL(origin).host.toLowerCase();
|
|
347
|
+
} catch (_unused2) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Strips the port, keeping IPv6 brackets — `[::1]:3000` is hostname `[::1]`. */
|
|
353
|
+
function hostnameOf(host) {
|
|
354
|
+
if (host.startsWith("[")) {
|
|
355
|
+
var end = host.indexOf("]");
|
|
356
|
+
return end === -1 ? host : host.slice(0, end + 1);
|
|
357
|
+
}
|
|
358
|
+
var colon = host.indexOf(":");
|
|
359
|
+
return colon === -1 ? host : host.slice(0, colon);
|
|
360
|
+
}
|
|
361
|
+
function readBearerToken(request) {
|
|
362
|
+
var _match$;
|
|
363
|
+
var header = request.headers.get("authorization");
|
|
364
|
+
if (!header) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
var match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
368
|
+
var token = match === null || match === void 0 || (_match$ = match[1]) === null || _match$ === void 0 ? void 0 : _match$.trim();
|
|
369
|
+
return token ? token : null;
|
|
370
|
+
}
|
|
371
|
+
function jsonResponse(status, body) {
|
|
372
|
+
return new Response(JSON.stringify(body), {
|
|
373
|
+
status: status,
|
|
374
|
+
headers: {
|
|
375
|
+
"Content-Type": "application/json"
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
exports.initValMcp = initValMcp;
|
|
123
381
|
exports.initValServer = initValServer;
|
|
@@ -3,7 +3,7 @@ import { _ as _slicedToArray } from '../../dist/slicedToArray-aa291011.esm.js';
|
|
|
3
3
|
import { _ as _asyncToGenerator, a as _regenerator, V as VERSION } from '../../dist/version-4d7b692c.esm.js';
|
|
4
4
|
import { _ as _objectSpread2 } from '../../dist/objectSpread2-60d1bd93.esm.js';
|
|
5
5
|
import { Internal } from '@valbuild/core';
|
|
6
|
-
import { createValApiRouter, createValServer } from '@valbuild/server';
|
|
6
|
+
import { createValApiRouter, createValServer, createValTools, initHandlerOptions } from '@valbuild/server';
|
|
7
7
|
import { NextResponse } from 'next/server';
|
|
8
8
|
import '../../dist/unsupportedIterableToArray-5baabfdc.esm.js';
|
|
9
9
|
import '../../dist/defineProperty-cca5affa.esm.js';
|
|
@@ -116,4 +116,261 @@ function initValServer(valModules, config, nextConfig) {
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Val's tools over MCP, and the two checks that have to happen before a request
|
|
121
|
+
* gets to them.
|
|
122
|
+
*
|
|
123
|
+
* Nothing here imports an MCP SDK. The app owns the transport — which SDK, which
|
|
124
|
+
* route, which framework — and this owns the parts that must not be re-decided
|
|
125
|
+
* per app: whether the request is allowed to reach the tools at all, and whose
|
|
126
|
+
* credential it carries. `docs/plans/mcp.md` Part A has the reasoning; the short
|
|
127
|
+
* version is that the SDK reorganised itself once already, and the security
|
|
128
|
+
* checks should not move when it does again.
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
function initValMcp(valModules, config, opts) {
|
|
132
|
+
var route = "/api/val"; // TODO: get from config, as initValServer does
|
|
133
|
+
var coreVersion = Internal.VERSION.core;
|
|
134
|
+
if (!coreVersion) {
|
|
135
|
+
throw new Error("Could not get @valbuild/core package version");
|
|
136
|
+
}
|
|
137
|
+
var nextVersion = VERSION;
|
|
138
|
+
if (!nextVersion) {
|
|
139
|
+
throw new Error("Could not get @valbuild/next package version");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Resolved once at module-eval time, awaited per request. The no-op catch is
|
|
143
|
+
// load bearing for the same reason it is in createValApiRouter: a config error
|
|
144
|
+
// on a promise with no handler attached becomes an unhandledRejection and
|
|
145
|
+
// takes the dev server down, and the error is reported per request below
|
|
146
|
+
// anyway.
|
|
147
|
+
var setupPromise = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
|
|
148
|
+
var options;
|
|
149
|
+
return _regenerator().w(function (_context) {
|
|
150
|
+
while (1) switch (_context.n) {
|
|
151
|
+
case 0:
|
|
152
|
+
_context.n = 1;
|
|
153
|
+
return initHandlerOptions(route, _objectSpread2(_objectSpread2({}, config), {}, {
|
|
154
|
+
versions: {
|
|
155
|
+
core: coreVersion,
|
|
156
|
+
next: nextVersion
|
|
157
|
+
}
|
|
158
|
+
}), config);
|
|
159
|
+
case 1:
|
|
160
|
+
options = _context.v;
|
|
161
|
+
return _context.a(2, {
|
|
162
|
+
mode: options.mode,
|
|
163
|
+
tools: createValTools(valModules, _objectSpread2(_objectSpread2({}, options), {}, {
|
|
164
|
+
formatter: opts === null || opts === void 0 ? void 0 : opts.formatter
|
|
165
|
+
}))
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}, _callee);
|
|
169
|
+
}))();
|
|
170
|
+
setupPromise["catch"](function () {
|
|
171
|
+
// handled per request
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
valMcpTools: function valMcpTools() {
|
|
175
|
+
return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
|
|
176
|
+
return _regenerator().w(function (_context2) {
|
|
177
|
+
while (1) switch (_context2.n) {
|
|
178
|
+
case 0:
|
|
179
|
+
_context2.n = 1;
|
|
180
|
+
return setupPromise;
|
|
181
|
+
case 1:
|
|
182
|
+
return _context2.a(2, _context2.v.tools);
|
|
183
|
+
}
|
|
184
|
+
}, _callee2);
|
|
185
|
+
}))();
|
|
186
|
+
},
|
|
187
|
+
valMcpAuthorize: function valMcpAuthorize(request) {
|
|
188
|
+
return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() {
|
|
189
|
+
var setup, refusal, pat, _t;
|
|
190
|
+
return _regenerator().w(function (_context3) {
|
|
191
|
+
while (1) switch (_context3.p = _context3.n) {
|
|
192
|
+
case 0:
|
|
193
|
+
_context3.p = 0;
|
|
194
|
+
_context3.n = 1;
|
|
195
|
+
return setupPromise;
|
|
196
|
+
case 1:
|
|
197
|
+
setup = _context3.v;
|
|
198
|
+
_context3.n = 3;
|
|
199
|
+
break;
|
|
200
|
+
case 2:
|
|
201
|
+
_context3.p = 2;
|
|
202
|
+
_t = _context3.v;
|
|
203
|
+
return _context3.a(2, {
|
|
204
|
+
status: "refused",
|
|
205
|
+
response: jsonResponse(500, {
|
|
206
|
+
error: "Val: could not start the Val MCP server",
|
|
207
|
+
details: _t instanceof Error ? _t.message : String(_t)
|
|
208
|
+
})
|
|
209
|
+
});
|
|
210
|
+
case 3:
|
|
211
|
+
if (!(request === undefined)) {
|
|
212
|
+
_context3.n = 4;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
return _context3.a(2, {
|
|
216
|
+
status: "refused",
|
|
217
|
+
response: jsonResponse(401, {
|
|
218
|
+
error: "Val: this MCP server needs the HTTP request to authorize a call, and none was available."
|
|
219
|
+
})
|
|
220
|
+
});
|
|
221
|
+
case 4:
|
|
222
|
+
refusal = refuseUnsafeRequest(request, setup.mode);
|
|
223
|
+
if (!refusal) {
|
|
224
|
+
_context3.n = 5;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
return _context3.a(2, {
|
|
228
|
+
status: "refused",
|
|
229
|
+
response: refusal
|
|
230
|
+
});
|
|
231
|
+
case 5:
|
|
232
|
+
pat = readBearerToken(request);
|
|
233
|
+
return _context3.a(2, {
|
|
234
|
+
status: "ok",
|
|
235
|
+
tools: setup.tools,
|
|
236
|
+
ctx: {
|
|
237
|
+
// Passed through unverified, deliberately: this app is not the
|
|
238
|
+
// authority on what a token may do, and the registry sends it to the
|
|
239
|
+
// backend that is. See `docs/plans/mcp.md` D.2.
|
|
240
|
+
auth: pat === null ? null : {
|
|
241
|
+
pat: pat
|
|
242
|
+
},
|
|
243
|
+
// Not the MCP session id. Val's patch `sessionId` names a Val AI
|
|
244
|
+
// session, and putting an unrelated id in it would claim a
|
|
245
|
+
// relationship that does not exist.
|
|
246
|
+
sessionId: null
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}, _callee3, null, [[0, 2]]);
|
|
251
|
+
}))();
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The two ways this route is dangerous, both refused here.
|
|
258
|
+
*
|
|
259
|
+
* 1. **Local filesystem mode outside development.** In fs mode there is no
|
|
260
|
+
* credential and no backend: the tools read and write the running process's
|
|
261
|
+
* own working tree, and every permission check Val has lives on the other
|
|
262
|
+
* side of a backend that is not in this path. Exposed on a deployed host,
|
|
263
|
+
* that is unauthenticated write access to the site's content for anyone who
|
|
264
|
+
* can reach the port. There is no configuration that makes it safe, so there
|
|
265
|
+
* is no flag to turn this off — a project that wants MCP in production wants
|
|
266
|
+
* proxy mode, where every call carries its caller's own token.
|
|
267
|
+
*
|
|
268
|
+
* 2. **A browser driving the local server.** A page on any origin can `fetch`
|
|
269
|
+
* `http://localhost:3000/api/mcp` while a developer has the app running, and
|
|
270
|
+
* with DNS rebinding it can do so with a `Host` of its own choosing. Neither
|
|
271
|
+
* needs a credential in fs mode. So a cross-origin `Origin` is refused, and
|
|
272
|
+
* in fs mode the request must actually be addressed to a loopback host.
|
|
273
|
+
*
|
|
274
|
+
* MCP clients are not browsers and send no `Origin`, so the check costs them
|
|
275
|
+
* nothing.
|
|
276
|
+
*/
|
|
277
|
+
function refuseUnsafeRequest(request, mode) {
|
|
278
|
+
if (mode === "fs" && process.env.NODE_ENV !== "development") {
|
|
279
|
+
return jsonResponse(403, {
|
|
280
|
+
error: "Val: the MCP endpoint is disabled. This project is running in local filesystem mode, where MCP calls are unauthenticated and write directly to the working tree, so it is only served in development. Configure Val for proxy mode to use MCP on a deployed host."
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
var host = requestHost(request);
|
|
284
|
+
var origin = request.headers.get("origin");
|
|
285
|
+
if (origin !== null) {
|
|
286
|
+
// `Origin: null` is refused along with the rest. It is the *opaque* origin —
|
|
287
|
+
// a sandboxed iframe, a `file://` page, some redirects — so it cannot be
|
|
288
|
+
// compared to anything, and "cannot be compared" has to mean refuse: a page
|
|
289
|
+
// that would fail the check can otherwise pass it by arranging to have no
|
|
290
|
+
// origin at all. Absent entirely is the case that is allowed, and that is
|
|
291
|
+
// the one MCP clients produce.
|
|
292
|
+
var originHost = origin === "null" ? null : hostOf(origin);
|
|
293
|
+
if (originHost === null || host === null || originHost !== host) {
|
|
294
|
+
return jsonResponse(403, {
|
|
295
|
+
error: "Val: refusing a cross-origin MCP request from ".concat(JSON.stringify(origin), ". MCP clients do not send an Origin header; a browser does.")
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (mode === "fs") {
|
|
300
|
+
var hostname = host === null ? null : hostnameOf(host);
|
|
301
|
+
if (hostname === null || !LOOPBACK_HOSTNAMES.has(hostname)) {
|
|
302
|
+
return jsonResponse(403, {
|
|
303
|
+
error: "Val: refusing an MCP request addressed to ".concat(JSON.stringify(host !== null && host !== void 0 ? host : "an unknown host"), ". In local filesystem mode this endpoint only answers on localhost, because a name that resolves to 127.0.0.1 is how a web page reaches a developer's own machine.")
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
var LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Which host the request was addressed to, as `hostname:port`.
|
|
313
|
+
*
|
|
314
|
+
* `Host` only, and deliberately **not** `X-Forwarded-Host`. The forwarded header
|
|
315
|
+
* is what a client asked for behind a proxy, but nothing stops a client sending
|
|
316
|
+
* it directly — so preferring it hands an attacker the value both checks below
|
|
317
|
+
* are decided on. `Host`, by contrast, a browser sets from the URL and page
|
|
318
|
+
* script cannot override, which is exactly the property the loopback check
|
|
319
|
+
* depends on.
|
|
320
|
+
*
|
|
321
|
+
* The cost is that behind a proxy that rewrites `Host`, a *browser* request
|
|
322
|
+
* whose `Origin` is the public name no longer matches. That is acceptable: such
|
|
323
|
+
* a request carries no personal access token, so proxy mode refuses it anyway,
|
|
324
|
+
* and a non-browser MCP client sends no `Origin` and never reaches the
|
|
325
|
+
* comparison. Trusting the forwarded header would need an explicit trusted-proxy
|
|
326
|
+
* configuration, which is a bigger thing than this needs.
|
|
327
|
+
*/
|
|
328
|
+
function requestHost(request) {
|
|
329
|
+
var host = request.headers.get("host");
|
|
330
|
+
if (host) {
|
|
331
|
+
return host.trim().toLowerCase();
|
|
332
|
+
}
|
|
333
|
+
// Last resort: the URL the framework saw.
|
|
334
|
+
try {
|
|
335
|
+
return new URL(request.url).host.toLowerCase();
|
|
336
|
+
} catch (_unused) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function hostOf(origin) {
|
|
341
|
+
try {
|
|
342
|
+
return new URL(origin).host.toLowerCase();
|
|
343
|
+
} catch (_unused2) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Strips the port, keeping IPv6 brackets — `[::1]:3000` is hostname `[::1]`. */
|
|
349
|
+
function hostnameOf(host) {
|
|
350
|
+
if (host.startsWith("[")) {
|
|
351
|
+
var end = host.indexOf("]");
|
|
352
|
+
return end === -1 ? host : host.slice(0, end + 1);
|
|
353
|
+
}
|
|
354
|
+
var colon = host.indexOf(":");
|
|
355
|
+
return colon === -1 ? host : host.slice(0, colon);
|
|
356
|
+
}
|
|
357
|
+
function readBearerToken(request) {
|
|
358
|
+
var _match$;
|
|
359
|
+
var header = request.headers.get("authorization");
|
|
360
|
+
if (!header) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
var match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
364
|
+
var token = match === null || match === void 0 || (_match$ = match[1]) === null || _match$ === void 0 ? void 0 : _match$.trim();
|
|
365
|
+
return token ? token : null;
|
|
366
|
+
}
|
|
367
|
+
function jsonResponse(status, body) {
|
|
368
|
+
return new Response(JSON.stringify(body), {
|
|
369
|
+
status: status,
|
|
370
|
+
headers: {
|
|
371
|
+
"Content-Type": "application/json"
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export { initValMcp, initValServer };
|