mcp-expose 1.0.0 → 1.1.1
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 +36 -1
- package/README.md +181 -32
- package/dist/adonisjs/index.cjs +366 -6
- package/dist/adonisjs/index.cjs.map +1 -1
- package/dist/adonisjs/index.d.cts +2 -1
- package/dist/adonisjs/index.d.ts +2 -1
- package/dist/adonisjs/index.js +366 -6
- package/dist/adonisjs/index.js.map +1 -1
- package/dist/express/index.cjs +364 -7
- package/dist/express/index.cjs.map +1 -1
- package/dist/express/index.d.cts +4 -2
- package/dist/express/index.d.ts +4 -2
- package/dist/express/index.js +364 -7
- package/dist/express/index.js.map +1 -1
- package/dist/fastify/index.cjs +373 -6
- package/dist/fastify/index.cjs.map +1 -1
- package/dist/fastify/index.d.cts +2 -1
- package/dist/fastify/index.d.ts +2 -1
- package/dist/fastify/index.js +373 -6
- package/dist/fastify/index.js.map +1 -1
- package/dist/hono/index.cjs +367 -6
- package/dist/hono/index.cjs.map +1 -1
- package/dist/hono/index.d.cts +2 -1
- package/dist/hono/index.d.ts +2 -1
- package/dist/hono/index.js +367 -6
- package/dist/hono/index.js.map +1 -1
- package/dist/index.cjs +371 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.js +367 -31
- package/dist/index.js.map +1 -1
- package/dist/koa/index.cjs +366 -6
- package/dist/koa/index.cjs.map +1 -1
- package/dist/koa/index.d.cts +2 -1
- package/dist/koa/index.d.ts +2 -1
- package/dist/koa/index.js +366 -6
- package/dist/koa/index.js.map +1 -1
- package/dist/nestjs/index.cjs +381 -7
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.d.cts +21 -7
- package/dist/nestjs/index.d.ts +21 -7
- package/dist/nestjs/index.js +382 -8
- package/dist/nestjs/index.js.map +1 -1
- package/dist/oauth/index.cjs +214 -0
- package/dist/oauth/index.cjs.map +1 -0
- package/dist/oauth/index.d.cts +69 -0
- package/dist/oauth/index.d.ts +69 -0
- package/dist/oauth/index.js +177 -0
- package/dist/oauth/index.js.map +1 -0
- package/dist/oauth-8RmQamGP.d.cts +75 -0
- package/dist/oauth-8RmQamGP.d.ts +75 -0
- package/dist/{server-fzkb-wJo.d.ts → server--mQVQq3A.d.cts} +81 -1
- package/dist/{server-fzkb-wJo.d.cts → server-BPDTsT2o.d.ts} +81 -1
- package/package.json +22 -2
package/dist/nestjs/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
3
3
|
|
|
4
4
|
// src/nestjs/index.ts
|
|
5
5
|
import { All, Controller, Inject, Injectable, Module, Req, Res, SetMetadata, UseGuards, VERSION_NEUTRAL } from "@nestjs/common";
|
|
6
|
-
import { ApplicationConfig, DiscoveryModule, DiscoveryService } from "@nestjs/core";
|
|
6
|
+
import { ApplicationConfig, DiscoveryModule, DiscoveryService, HttpAdapterHost } from "@nestjs/core";
|
|
7
7
|
|
|
8
8
|
// src/core/dispatch.ts
|
|
9
9
|
function buildUrl(base, req) {
|
|
@@ -60,6 +60,38 @@ function loopbackBaseUrl(req) {
|
|
|
60
60
|
}
|
|
61
61
|
__name(loopbackBaseUrl, "loopbackBaseUrl");
|
|
62
62
|
|
|
63
|
+
// src/core/result.ts
|
|
64
|
+
function isToolResult(value) {
|
|
65
|
+
const content = value?.content;
|
|
66
|
+
return !!value && typeof value === "object" && Array.isArray(content) && content.length > 0 && content.every((c) => !!c && typeof c === "object" && typeof c.type === "string");
|
|
67
|
+
}
|
|
68
|
+
__name(isToolResult, "isToolResult");
|
|
69
|
+
function toToolResult(value) {
|
|
70
|
+
if (isToolResult(value)) return value;
|
|
71
|
+
if (typeof value === "string") return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: value
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
const text = JSON.stringify(value ?? null);
|
|
80
|
+
const result = {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: "text",
|
|
84
|
+
text
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
};
|
|
88
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
89
|
+
result.structuredContent = value;
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
__name(toToolResult, "toToolResult");
|
|
94
|
+
|
|
63
95
|
// src/core/schema.ts
|
|
64
96
|
function isStandard(s) {
|
|
65
97
|
return typeof s === "object" && s !== null && "~standard" in s;
|
|
@@ -285,9 +317,7 @@ function splitArgs(args, mapping) {
|
|
|
285
317
|
}
|
|
286
318
|
__name(splitArgs, "splitArgs");
|
|
287
319
|
function responseToResult(res, maxChars) {
|
|
288
|
-
|
|
289
|
-
if (text.length > maxChars) text = `${text.slice(0, maxChars)}
|
|
290
|
-
\u2026[truncated ${text.length - maxChars} chars]`;
|
|
320
|
+
const text = truncate(res.body, maxChars);
|
|
291
321
|
const isError = res.status >= 400;
|
|
292
322
|
const result = {
|
|
293
323
|
content: [
|
|
@@ -312,6 +342,58 @@ ${text}` : text || `HTTP ${res.status}`
|
|
|
312
342
|
return result;
|
|
313
343
|
}
|
|
314
344
|
__name(responseToResult, "responseToResult");
|
|
345
|
+
function safeDecode(segment) {
|
|
346
|
+
try {
|
|
347
|
+
return decodeURIComponent(segment);
|
|
348
|
+
} catch {
|
|
349
|
+
return segment;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
__name(safeDecode, "safeDecode");
|
|
353
|
+
function truncate(text, maxChars) {
|
|
354
|
+
return text.length > maxChars ? `${text.slice(0, maxChars)}
|
|
355
|
+
\u2026[truncated ${text.length - maxChars} chars]` : text;
|
|
356
|
+
}
|
|
357
|
+
__name(truncate, "truncate");
|
|
358
|
+
function toRouteToolResponse(res) {
|
|
359
|
+
let json;
|
|
360
|
+
if ((res.headers["content-type"] ?? "").includes("json") && res.body) {
|
|
361
|
+
try {
|
|
362
|
+
json = JSON.parse(res.body);
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
...res,
|
|
368
|
+
json,
|
|
369
|
+
ok: res.status < 400
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
__name(toRouteToolResponse, "toRouteToolResponse");
|
|
373
|
+
function transformedResult(value, res, maxChars) {
|
|
374
|
+
const explicit = isToolResult(value);
|
|
375
|
+
const result = toToolResult(value);
|
|
376
|
+
if (!explicit && !res.ok) {
|
|
377
|
+
result.isError = true;
|
|
378
|
+
delete result.structuredContent;
|
|
379
|
+
}
|
|
380
|
+
let truncated = false;
|
|
381
|
+
const content = result.content.map((c) => {
|
|
382
|
+
if (c.type !== "text" || c.text.length <= maxChars) return c;
|
|
383
|
+
truncated = true;
|
|
384
|
+
return {
|
|
385
|
+
...c,
|
|
386
|
+
text: truncate(c.text, maxChars)
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
if (!truncated) return result;
|
|
390
|
+
const { structuredContent: _dropped, ...rest } = result;
|
|
391
|
+
return {
|
|
392
|
+
...rest,
|
|
393
|
+
content
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
__name(transformedResult, "transformedResult");
|
|
315
397
|
function pickForwardHeaders(ctx, names) {
|
|
316
398
|
const out = {};
|
|
317
399
|
for (const name of names) {
|
|
@@ -327,6 +409,7 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
|
|
|
327
409
|
const { schema, mapping } = buildInputSchema(route, opts);
|
|
328
410
|
const forward = serverOpts.forwardHeaders ?? DEFAULT_FORWARD_HEADERS;
|
|
329
411
|
const maxChars = serverOpts.maxResponseChars ?? 1e5;
|
|
412
|
+
const transform = opts.transformResponse ?? serverOpts.transformResponse;
|
|
330
413
|
return {
|
|
331
414
|
name,
|
|
332
415
|
title: opts.title,
|
|
@@ -336,6 +419,7 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
|
|
|
336
419
|
...defaultAnnotations(method),
|
|
337
420
|
...opts.annotations
|
|
338
421
|
},
|
|
422
|
+
scopes: opts.scopes,
|
|
339
423
|
validate: toValidator(opts.input),
|
|
340
424
|
async handler(args, ctx) {
|
|
341
425
|
const missing = opts.input ? void 0 : checkRequired(schema, args);
|
|
@@ -371,19 +455,167 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
|
|
|
371
455
|
]
|
|
372
456
|
};
|
|
373
457
|
}
|
|
374
|
-
const
|
|
458
|
+
const request = {
|
|
375
459
|
method,
|
|
376
460
|
path,
|
|
377
461
|
query,
|
|
378
462
|
body,
|
|
379
463
|
headers
|
|
380
|
-
}
|
|
381
|
-
|
|
464
|
+
};
|
|
465
|
+
const res = await dispatch(request, ctx);
|
|
466
|
+
if (!transform) return responseToResult(res, maxChars);
|
|
467
|
+
const { "content-type": _ct, ...baseHeaders } = headers;
|
|
468
|
+
const hookCtx = {
|
|
469
|
+
tool: name,
|
|
470
|
+
args,
|
|
471
|
+
request,
|
|
472
|
+
toolContext: ctx,
|
|
473
|
+
defaultResult: /* @__PURE__ */ __name(() => responseToResult(res, maxChars), "defaultResult"),
|
|
474
|
+
async callRoute(r) {
|
|
475
|
+
if (!r.path.startsWith("/")) {
|
|
476
|
+
throw new Error(`callRoute: path must start with "/", got ${JSON.stringify(r.path)}`);
|
|
477
|
+
}
|
|
478
|
+
const params = r.params ?? {};
|
|
479
|
+
const missing2 = pathParams(r.path).filter((p) => params[p] === void 0 && !r.path.includes(`:${p}?`));
|
|
480
|
+
if (missing2.length) throw new Error(`callRoute: missing params for ${r.path}: ${missing2.join(", ")}`);
|
|
481
|
+
const subPath = interpolatePath(r.path, params);
|
|
482
|
+
if (subPath.split("/").some((seg) => [
|
|
483
|
+
".",
|
|
484
|
+
".."
|
|
485
|
+
].includes(safeDecode(seg)))) {
|
|
486
|
+
throw new Error(`callRoute: path must not contain "." or ".." segments: ${JSON.stringify(subPath)}`);
|
|
487
|
+
}
|
|
488
|
+
const sub = {
|
|
489
|
+
method: (r.method ?? "GET").toUpperCase(),
|
|
490
|
+
path: subPath,
|
|
491
|
+
query: r.query ?? {},
|
|
492
|
+
body: r.body,
|
|
493
|
+
headers: {
|
|
494
|
+
...baseHeaders,
|
|
495
|
+
...r.headers
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
if (sub.body !== void 0) sub.headers["content-type"] = "application/json";
|
|
499
|
+
return toRouteToolResponse(await dispatch(sub, ctx));
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
const response = toRouteToolResponse(res);
|
|
503
|
+
const value = await transform(response, hookCtx);
|
|
504
|
+
return value === void 0 ? responseToResult(res, maxChars) : transformedResult(value, response, maxChars);
|
|
382
505
|
}
|
|
383
506
|
};
|
|
384
507
|
}
|
|
385
508
|
__name(createRouteTool, "createRouteTool");
|
|
386
509
|
|
|
510
|
+
// src/core/oauth.ts
|
|
511
|
+
var PROTECTED_RESOURCE_WELL_KNOWN = "/.well-known/oauth-protected-resource";
|
|
512
|
+
function protectedResourceMetadataPaths(resource) {
|
|
513
|
+
const pathname = new URL(resource).pathname.replace(/\/+$/, "");
|
|
514
|
+
return pathname ? [
|
|
515
|
+
PROTECTED_RESOURCE_WELL_KNOWN + pathname,
|
|
516
|
+
PROTECTED_RESOURCE_WELL_KNOWN
|
|
517
|
+
] : [
|
|
518
|
+
PROTECTED_RESOURCE_WELL_KNOWN
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
__name(protectedResourceMetadataPaths, "protectedResourceMetadataPaths");
|
|
522
|
+
function protectedResourceMetadataUrl(resource) {
|
|
523
|
+
return new URL(protectedResourceMetadataPaths(resource)[0], resource).href;
|
|
524
|
+
}
|
|
525
|
+
__name(protectedResourceMetadataUrl, "protectedResourceMetadataUrl");
|
|
526
|
+
function validateOAuthOptions(o) {
|
|
527
|
+
let url;
|
|
528
|
+
try {
|
|
529
|
+
url = new URL(o.resource);
|
|
530
|
+
} catch {
|
|
531
|
+
throw new Error(`mcp-expose: oauth.resource must be an absolute URL, got ${JSON.stringify(o.resource)}`);
|
|
532
|
+
}
|
|
533
|
+
if (url.hash) throw new Error("mcp-expose: oauth.resource must not contain a fragment");
|
|
534
|
+
if (!o.authorizationServers?.length) throw new Error("mcp-expose: oauth.authorizationServers must not be empty");
|
|
535
|
+
if (typeof o.verifyToken !== "function") throw new Error("mcp-expose: oauth.verifyToken is required");
|
|
536
|
+
}
|
|
537
|
+
__name(validateOAuthOptions, "validateOAuthOptions");
|
|
538
|
+
var quote = /* @__PURE__ */ __name((v) => `"${v.replace(/["\\]/g, "\\$&")}"`, "quote");
|
|
539
|
+
function bearerChallenge(resource, params = {}) {
|
|
540
|
+
const parts = [];
|
|
541
|
+
if (params.error) parts.push(`error=${quote(params.error)}`);
|
|
542
|
+
if (params.errorDescription) parts.push(`error_description=${quote(params.errorDescription)}`);
|
|
543
|
+
if (params.scope?.length) parts.push(`scope=${quote(params.scope.join(" "))}`);
|
|
544
|
+
parts.push(`resource_metadata=${quote(protectedResourceMetadataUrl(resource))}`);
|
|
545
|
+
return `Bearer ${parts.join(", ")}`;
|
|
546
|
+
}
|
|
547
|
+
__name(bearerChallenge, "bearerChallenge");
|
|
548
|
+
function bearerToken(headers) {
|
|
549
|
+
const m = /^Bearer[ ]+([^\s,]+)\s*$/i.exec(headers.authorization ?? "");
|
|
550
|
+
return m?.[1];
|
|
551
|
+
}
|
|
552
|
+
__name(bearerToken, "bearerToken");
|
|
553
|
+
async function authenticate(o, headers) {
|
|
554
|
+
const token = bearerToken(headers);
|
|
555
|
+
if (!token) {
|
|
556
|
+
return {
|
|
557
|
+
ok: false,
|
|
558
|
+
status: 401,
|
|
559
|
+
error: "unauthorized",
|
|
560
|
+
description: "Authorization required",
|
|
561
|
+
challenge: bearerChallenge(o.resource, {
|
|
562
|
+
scope: o.requiredScopes
|
|
563
|
+
})
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
let auth;
|
|
567
|
+
try {
|
|
568
|
+
auth = await o.verifyToken(token, {
|
|
569
|
+
resource: o.resource,
|
|
570
|
+
headers
|
|
571
|
+
});
|
|
572
|
+
} catch {
|
|
573
|
+
auth = void 0;
|
|
574
|
+
}
|
|
575
|
+
if (!auth) return deny(o, 401, "invalid_token", "The access token is invalid");
|
|
576
|
+
if (auth.expiresAt !== void 0 && auth.expiresAt * 1e3 <= Date.now()) {
|
|
577
|
+
return deny(o, 401, "invalid_token", "The access token has expired");
|
|
578
|
+
}
|
|
579
|
+
const missing = missingScopes(auth, o.requiredScopes);
|
|
580
|
+
if (missing.length) {
|
|
581
|
+
const scope = [
|
|
582
|
+
.../* @__PURE__ */ new Set([
|
|
583
|
+
...auth.scopes ?? [],
|
|
584
|
+
...o.requiredScopes ?? []
|
|
585
|
+
])
|
|
586
|
+
];
|
|
587
|
+
return deny(o, 403, "insufficient_scope", `Missing scope: ${missing.join(" ")}`, scope);
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
ok: true,
|
|
591
|
+
auth: {
|
|
592
|
+
...auth,
|
|
593
|
+
scopes: auth.scopes ?? []
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
__name(authenticate, "authenticate");
|
|
598
|
+
function deny(o, status, error, description, scope) {
|
|
599
|
+
return {
|
|
600
|
+
ok: false,
|
|
601
|
+
status,
|
|
602
|
+
error,
|
|
603
|
+
description,
|
|
604
|
+
challenge: bearerChallenge(o.resource, {
|
|
605
|
+
error,
|
|
606
|
+
errorDescription: description,
|
|
607
|
+
scope
|
|
608
|
+
})
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
__name(deny, "deny");
|
|
612
|
+
function missingScopes(auth, required) {
|
|
613
|
+
if (!required?.length) return [];
|
|
614
|
+
const granted = new Set(auth?.scopes ?? []);
|
|
615
|
+
return required.filter((s) => !granted.has(s));
|
|
616
|
+
}
|
|
617
|
+
__name(missingScopes, "missingScopes");
|
|
618
|
+
|
|
387
619
|
// src/core/server.ts
|
|
388
620
|
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
389
621
|
"2025-11-25",
|
|
@@ -418,6 +650,7 @@ var McpServer = class {
|
|
|
418
650
|
loaders = [];
|
|
419
651
|
constructor(options) {
|
|
420
652
|
this.options = options;
|
|
653
|
+
if (options.oauth) validateOAuthOptions(options.oauth);
|
|
421
654
|
}
|
|
422
655
|
/** Register a tool. Throws on duplicate names. */
|
|
423
656
|
addTool(tool) {
|
|
@@ -552,6 +785,79 @@ var McpServer = class {
|
|
|
552
785
|
return (allowed ?? []).includes(origin);
|
|
553
786
|
}
|
|
554
787
|
/**
|
|
788
|
+
* Paths on which adapters serve the OAuth Protected Resource Metadata
|
|
789
|
+
* (empty when `oauth` is not configured). They must be public: no auth middleware.
|
|
790
|
+
*/
|
|
791
|
+
get oauthMetadataPaths() {
|
|
792
|
+
return this.options.oauth ? protectedResourceMetadataPaths(this.options.oauth.resource) : [];
|
|
793
|
+
}
|
|
794
|
+
/** The OAuth Protected Resource Metadata document (RFC 9728). */
|
|
795
|
+
async protectedResourceMetadata() {
|
|
796
|
+
const o = this.options.oauth;
|
|
797
|
+
if (!o) throw new Error("mcp-expose: the oauth option is not configured");
|
|
798
|
+
await this.load();
|
|
799
|
+
const scopes = o.scopesSupported ?? [
|
|
800
|
+
.../* @__PURE__ */ new Set([
|
|
801
|
+
...o.requiredScopes ?? [],
|
|
802
|
+
...[
|
|
803
|
+
...this.tools.values()
|
|
804
|
+
].flatMap((t) => t.scopes ?? [])
|
|
805
|
+
])
|
|
806
|
+
];
|
|
807
|
+
return {
|
|
808
|
+
resource: o.resource,
|
|
809
|
+
authorization_servers: o.authorizationServers,
|
|
810
|
+
...scopes.length ? {
|
|
811
|
+
scopes_supported: scopes
|
|
812
|
+
} : {},
|
|
813
|
+
bearer_methods_supported: [
|
|
814
|
+
"header"
|
|
815
|
+
],
|
|
816
|
+
...o.resourceName ? {
|
|
817
|
+
resource_name: o.resourceName
|
|
818
|
+
} : {},
|
|
819
|
+
...o.resourceDocumentation ? {
|
|
820
|
+
resource_documentation: o.resourceDocumentation
|
|
821
|
+
} : {},
|
|
822
|
+
...o.metadata
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
/** HTTP handler for the metadata paths. Public and CORS-enabled so browser-based clients can read it. */
|
|
826
|
+
async handleMetadataHttp(req) {
|
|
827
|
+
const cors = {
|
|
828
|
+
"access-control-allow-origin": "*",
|
|
829
|
+
"access-control-allow-methods": "GET, OPTIONS"
|
|
830
|
+
};
|
|
831
|
+
const method = req.method.toUpperCase();
|
|
832
|
+
if (!this.options.oauth) return {
|
|
833
|
+
status: 404,
|
|
834
|
+
headers: {}
|
|
835
|
+
};
|
|
836
|
+
if (method === "OPTIONS") return {
|
|
837
|
+
status: 204,
|
|
838
|
+
headers: {
|
|
839
|
+
...cors,
|
|
840
|
+
"access-control-allow-headers": "*"
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
if (method !== "GET" && method !== "HEAD") return {
|
|
844
|
+
status: 405,
|
|
845
|
+
headers: {
|
|
846
|
+
allow: "GET, OPTIONS"
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
const body = JSON.stringify(await this.protectedResourceMetadata());
|
|
850
|
+
return {
|
|
851
|
+
status: 200,
|
|
852
|
+
headers: {
|
|
853
|
+
...cors,
|
|
854
|
+
"content-type": "application/json",
|
|
855
|
+
"cache-control": "public, max-age=3600"
|
|
856
|
+
},
|
|
857
|
+
body: method === "HEAD" ? void 0 : body
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
555
861
|
* Framework-neutral Streamable HTTP handler. Adapters convert their request
|
|
556
862
|
* into McpHttpRequest, call this, and write the McpHttpResponse back.
|
|
557
863
|
*/
|
|
@@ -566,6 +872,15 @@ var McpServer = class {
|
|
|
566
872
|
body: JSON.stringify(fail(null, INVALID_REQUEST, "Origin not allowed"))
|
|
567
873
|
};
|
|
568
874
|
}
|
|
875
|
+
const oauth = this.options.oauth;
|
|
876
|
+
if (oauth) {
|
|
877
|
+
const outcome = await authenticate(oauth, req.headers);
|
|
878
|
+
if (!outcome.ok) return authError(outcome);
|
|
879
|
+
ctx = {
|
|
880
|
+
...ctx,
|
|
881
|
+
auth: outcome.auth
|
|
882
|
+
};
|
|
883
|
+
}
|
|
569
884
|
if (req.method.toUpperCase() !== "POST") {
|
|
570
885
|
return {
|
|
571
886
|
status: 405,
|
|
@@ -586,6 +901,27 @@ var McpServer = class {
|
|
|
586
901
|
};
|
|
587
902
|
}
|
|
588
903
|
}
|
|
904
|
+
if (oauth) {
|
|
905
|
+
await this.load();
|
|
906
|
+
const calls = Array.isArray(body) ? body : [
|
|
907
|
+
body
|
|
908
|
+
];
|
|
909
|
+
for (const m of calls) {
|
|
910
|
+
if (m?.method !== "tools/call") continue;
|
|
911
|
+
const tool = this.tools.get(m.params?.name);
|
|
912
|
+
const missing = missingScopes(ctx.auth, tool?.scopes);
|
|
913
|
+
if (missing.length) {
|
|
914
|
+
const scope = [
|
|
915
|
+
.../* @__PURE__ */ new Set([
|
|
916
|
+
...ctx.auth?.scopes ?? [],
|
|
917
|
+
...oauth.requiredScopes ?? [],
|
|
918
|
+
...tool?.scopes ?? []
|
|
919
|
+
])
|
|
920
|
+
];
|
|
921
|
+
return authError(deny(oauth, 403, "insufficient_scope", `Tool "${tool.name}" needs scope: ${missing.join(" ")}`, scope));
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
589
925
|
const response = await this.handleMessage(body, ctx);
|
|
590
926
|
if (response === null) return {
|
|
591
927
|
status: 202,
|
|
@@ -598,6 +934,20 @@ var McpServer = class {
|
|
|
598
934
|
};
|
|
599
935
|
}
|
|
600
936
|
};
|
|
937
|
+
function authError(o) {
|
|
938
|
+
return {
|
|
939
|
+
status: o.status,
|
|
940
|
+
headers: {
|
|
941
|
+
"content-type": "application/json",
|
|
942
|
+
"www-authenticate": o.challenge
|
|
943
|
+
},
|
|
944
|
+
body: JSON.stringify({
|
|
945
|
+
error: o.error,
|
|
946
|
+
error_description: o.description
|
|
947
|
+
})
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
__name(authError, "authError");
|
|
601
951
|
|
|
602
952
|
// src/nestjs/dto-schema.ts
|
|
603
953
|
var PRIMITIVES = /* @__PURE__ */ new Map([
|
|
@@ -806,11 +1156,13 @@ var McpService = class {
|
|
|
806
1156
|
discovery;
|
|
807
1157
|
appConfig;
|
|
808
1158
|
options;
|
|
1159
|
+
adapterHost;
|
|
809
1160
|
server;
|
|
810
|
-
constructor(discovery, appConfig, options) {
|
|
1161
|
+
constructor(discovery, appConfig, options, adapterHost) {
|
|
811
1162
|
this.discovery = discovery;
|
|
812
1163
|
this.appConfig = appConfig;
|
|
813
1164
|
this.options = options;
|
|
1165
|
+
this.adapterHost = adapterHost;
|
|
814
1166
|
this.server = new McpServer(options);
|
|
815
1167
|
for (const t of options.tools ?? []) this.server.addTool(t);
|
|
816
1168
|
const dispatch = createFetchDispatcher({
|
|
@@ -823,6 +1175,26 @@ var McpService = class {
|
|
|
823
1175
|
}
|
|
824
1176
|
});
|
|
825
1177
|
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Serve the OAuth Protected Resource Metadata at the root of the app, outside
|
|
1180
|
+
* the global prefix and guards (it must be public). Runs before Nest registers
|
|
1181
|
+
* its own routes.
|
|
1182
|
+
*/
|
|
1183
|
+
onModuleInit() {
|
|
1184
|
+
const adapter = this.adapterHost?.httpAdapter;
|
|
1185
|
+
if (!adapter) return;
|
|
1186
|
+
for (const path of this.server.oauthMetadataPaths) {
|
|
1187
|
+
const handler = /* @__PURE__ */ __name((method) => async (_req, res) => {
|
|
1188
|
+
const out = await this.server.handleMetadataHttp({
|
|
1189
|
+
method
|
|
1190
|
+
});
|
|
1191
|
+
for (const [k, v] of Object.entries(out.headers)) adapter.setHeader(res, k, v);
|
|
1192
|
+
adapter.reply(res, out.body, out.status);
|
|
1193
|
+
}, "handler");
|
|
1194
|
+
adapter.get(path, handler("GET"));
|
|
1195
|
+
adapter.options(path, handler("OPTIONS"));
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
826
1198
|
globalPrefix() {
|
|
827
1199
|
return this.appConfig.getGlobalPrefix?.() ?? "";
|
|
828
1200
|
}
|
|
@@ -955,6 +1327,7 @@ Injectable()(McpService);
|
|
|
955
1327
|
Inject(DiscoveryService)(McpService, void 0, 0);
|
|
956
1328
|
Inject(ApplicationConfig)(McpService, void 0, 1);
|
|
957
1329
|
Inject(MCP_MODULE_OPTIONS)(McpService, void 0, 2);
|
|
1330
|
+
Inject(HttpAdapterHost)(McpService, void 0, 3);
|
|
958
1331
|
function createMcpController(options) {
|
|
959
1332
|
let McpController = class McpController {
|
|
960
1333
|
static {
|
|
@@ -997,6 +1370,7 @@ function createMcpController(options) {
|
|
|
997
1370
|
Req()(proto, "handle", 0);
|
|
998
1371
|
Res()(proto, "handle", 1);
|
|
999
1372
|
if (options.guards?.length) UseGuards(...options.guards)(McpController);
|
|
1373
|
+
for (const decorate of options.decorators ?? []) decorate(McpController);
|
|
1000
1374
|
return McpController;
|
|
1001
1375
|
}
|
|
1002
1376
|
__name(createMcpController, "createMcpController");
|