mcp-expose 1.0.0 → 1.1.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/CHANGELOG.md +19 -1
- package/README.md +102 -13
- package/dist/adonisjs/index.cjs +238 -0
- 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 +238 -0
- package/dist/adonisjs/index.js.map +1 -1
- package/dist/express/index.cjs +236 -1
- 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 +236 -1
- package/dist/express/index.js.map +1 -1
- package/dist/fastify/index.cjs +245 -0
- 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 +245 -0
- package/dist/fastify/index.js.map +1 -1
- package/dist/hono/index.cjs +239 -0
- 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 +239 -0
- package/dist/hono/index.js.map +1 -1
- package/dist/index.cjs +243 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +239 -0
- package/dist/index.js.map +1 -1
- package/dist/koa/index.cjs +238 -0
- 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 +238 -0
- package/dist/koa/index.js.map +1 -1
- package/dist/nestjs/index.cjs +253 -1
- 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 +254 -2
- 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-BFQ_Hdh9.d.cts} +27 -0
- package/dist/{server-fzkb-wJo.d.cts → server-BRw1E2xy.d.ts} +27 -0
- package/package.json +21 -2
package/dist/nestjs/index.cjs
CHANGED
|
@@ -373,6 +373,7 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
|
|
|
373
373
|
...defaultAnnotations(method),
|
|
374
374
|
...opts.annotations
|
|
375
375
|
},
|
|
376
|
+
scopes: opts.scopes,
|
|
376
377
|
validate: toValidator(opts.input),
|
|
377
378
|
async handler(args, ctx) {
|
|
378
379
|
const missing = opts.input ? void 0 : checkRequired(schema, args);
|
|
@@ -421,6 +422,115 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
|
|
|
421
422
|
}
|
|
422
423
|
__name(createRouteTool, "createRouteTool");
|
|
423
424
|
|
|
425
|
+
// src/core/oauth.ts
|
|
426
|
+
var PROTECTED_RESOURCE_WELL_KNOWN = "/.well-known/oauth-protected-resource";
|
|
427
|
+
function protectedResourceMetadataPaths(resource) {
|
|
428
|
+
const pathname = new URL(resource).pathname.replace(/\/+$/, "");
|
|
429
|
+
return pathname ? [
|
|
430
|
+
PROTECTED_RESOURCE_WELL_KNOWN + pathname,
|
|
431
|
+
PROTECTED_RESOURCE_WELL_KNOWN
|
|
432
|
+
] : [
|
|
433
|
+
PROTECTED_RESOURCE_WELL_KNOWN
|
|
434
|
+
];
|
|
435
|
+
}
|
|
436
|
+
__name(protectedResourceMetadataPaths, "protectedResourceMetadataPaths");
|
|
437
|
+
function protectedResourceMetadataUrl(resource) {
|
|
438
|
+
return new URL(protectedResourceMetadataPaths(resource)[0], resource).href;
|
|
439
|
+
}
|
|
440
|
+
__name(protectedResourceMetadataUrl, "protectedResourceMetadataUrl");
|
|
441
|
+
function validateOAuthOptions(o) {
|
|
442
|
+
let url;
|
|
443
|
+
try {
|
|
444
|
+
url = new URL(o.resource);
|
|
445
|
+
} catch {
|
|
446
|
+
throw new Error(`mcp-expose: oauth.resource must be an absolute URL, got ${JSON.stringify(o.resource)}`);
|
|
447
|
+
}
|
|
448
|
+
if (url.hash) throw new Error("mcp-expose: oauth.resource must not contain a fragment");
|
|
449
|
+
if (!o.authorizationServers?.length) throw new Error("mcp-expose: oauth.authorizationServers must not be empty");
|
|
450
|
+
if (typeof o.verifyToken !== "function") throw new Error("mcp-expose: oauth.verifyToken is required");
|
|
451
|
+
}
|
|
452
|
+
__name(validateOAuthOptions, "validateOAuthOptions");
|
|
453
|
+
var quote = /* @__PURE__ */ __name((v) => `"${v.replace(/["\\]/g, "\\$&")}"`, "quote");
|
|
454
|
+
function bearerChallenge(resource, params = {}) {
|
|
455
|
+
const parts = [];
|
|
456
|
+
if (params.error) parts.push(`error=${quote(params.error)}`);
|
|
457
|
+
if (params.errorDescription) parts.push(`error_description=${quote(params.errorDescription)}`);
|
|
458
|
+
if (params.scope?.length) parts.push(`scope=${quote(params.scope.join(" "))}`);
|
|
459
|
+
parts.push(`resource_metadata=${quote(protectedResourceMetadataUrl(resource))}`);
|
|
460
|
+
return `Bearer ${parts.join(", ")}`;
|
|
461
|
+
}
|
|
462
|
+
__name(bearerChallenge, "bearerChallenge");
|
|
463
|
+
function bearerToken(headers) {
|
|
464
|
+
const m = /^Bearer[ ]+([^\s,]+)\s*$/i.exec(headers.authorization ?? "");
|
|
465
|
+
return m?.[1];
|
|
466
|
+
}
|
|
467
|
+
__name(bearerToken, "bearerToken");
|
|
468
|
+
async function authenticate(o, headers) {
|
|
469
|
+
const token = bearerToken(headers);
|
|
470
|
+
if (!token) {
|
|
471
|
+
return {
|
|
472
|
+
ok: false,
|
|
473
|
+
status: 401,
|
|
474
|
+
error: "unauthorized",
|
|
475
|
+
description: "Authorization required",
|
|
476
|
+
challenge: bearerChallenge(o.resource, {
|
|
477
|
+
scope: o.requiredScopes
|
|
478
|
+
})
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
let auth;
|
|
482
|
+
try {
|
|
483
|
+
auth = await o.verifyToken(token, {
|
|
484
|
+
resource: o.resource,
|
|
485
|
+
headers
|
|
486
|
+
});
|
|
487
|
+
} catch {
|
|
488
|
+
auth = void 0;
|
|
489
|
+
}
|
|
490
|
+
if (!auth) return deny(o, 401, "invalid_token", "The access token is invalid");
|
|
491
|
+
if (auth.expiresAt !== void 0 && auth.expiresAt * 1e3 <= Date.now()) {
|
|
492
|
+
return deny(o, 401, "invalid_token", "The access token has expired");
|
|
493
|
+
}
|
|
494
|
+
const missing = missingScopes(auth, o.requiredScopes);
|
|
495
|
+
if (missing.length) {
|
|
496
|
+
const scope = [
|
|
497
|
+
.../* @__PURE__ */ new Set([
|
|
498
|
+
...auth.scopes ?? [],
|
|
499
|
+
...o.requiredScopes ?? []
|
|
500
|
+
])
|
|
501
|
+
];
|
|
502
|
+
return deny(o, 403, "insufficient_scope", `Missing scope: ${missing.join(" ")}`, scope);
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
ok: true,
|
|
506
|
+
auth: {
|
|
507
|
+
...auth,
|
|
508
|
+
scopes: auth.scopes ?? []
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
__name(authenticate, "authenticate");
|
|
513
|
+
function deny(o, status, error, description, scope) {
|
|
514
|
+
return {
|
|
515
|
+
ok: false,
|
|
516
|
+
status,
|
|
517
|
+
error,
|
|
518
|
+
description,
|
|
519
|
+
challenge: bearerChallenge(o.resource, {
|
|
520
|
+
error,
|
|
521
|
+
errorDescription: description,
|
|
522
|
+
scope
|
|
523
|
+
})
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
__name(deny, "deny");
|
|
527
|
+
function missingScopes(auth, required) {
|
|
528
|
+
if (!required?.length) return [];
|
|
529
|
+
const granted = new Set(auth?.scopes ?? []);
|
|
530
|
+
return required.filter((s) => !granted.has(s));
|
|
531
|
+
}
|
|
532
|
+
__name(missingScopes, "missingScopes");
|
|
533
|
+
|
|
424
534
|
// src/core/server.ts
|
|
425
535
|
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
426
536
|
"2025-11-25",
|
|
@@ -455,6 +565,7 @@ var McpServer = class {
|
|
|
455
565
|
loaders = [];
|
|
456
566
|
constructor(options) {
|
|
457
567
|
this.options = options;
|
|
568
|
+
if (options.oauth) validateOAuthOptions(options.oauth);
|
|
458
569
|
}
|
|
459
570
|
/** Register a tool. Throws on duplicate names. */
|
|
460
571
|
addTool(tool) {
|
|
@@ -589,6 +700,79 @@ var McpServer = class {
|
|
|
589
700
|
return (allowed ?? []).includes(origin);
|
|
590
701
|
}
|
|
591
702
|
/**
|
|
703
|
+
* Paths on which adapters serve the OAuth Protected Resource Metadata
|
|
704
|
+
* (empty when `oauth` is not configured). They must be public: no auth middleware.
|
|
705
|
+
*/
|
|
706
|
+
get oauthMetadataPaths() {
|
|
707
|
+
return this.options.oauth ? protectedResourceMetadataPaths(this.options.oauth.resource) : [];
|
|
708
|
+
}
|
|
709
|
+
/** The OAuth Protected Resource Metadata document (RFC 9728). */
|
|
710
|
+
async protectedResourceMetadata() {
|
|
711
|
+
const o = this.options.oauth;
|
|
712
|
+
if (!o) throw new Error("mcp-expose: the oauth option is not configured");
|
|
713
|
+
await this.load();
|
|
714
|
+
const scopes = o.scopesSupported ?? [
|
|
715
|
+
.../* @__PURE__ */ new Set([
|
|
716
|
+
...o.requiredScopes ?? [],
|
|
717
|
+
...[
|
|
718
|
+
...this.tools.values()
|
|
719
|
+
].flatMap((t) => t.scopes ?? [])
|
|
720
|
+
])
|
|
721
|
+
];
|
|
722
|
+
return {
|
|
723
|
+
resource: o.resource,
|
|
724
|
+
authorization_servers: o.authorizationServers,
|
|
725
|
+
...scopes.length ? {
|
|
726
|
+
scopes_supported: scopes
|
|
727
|
+
} : {},
|
|
728
|
+
bearer_methods_supported: [
|
|
729
|
+
"header"
|
|
730
|
+
],
|
|
731
|
+
...o.resourceName ? {
|
|
732
|
+
resource_name: o.resourceName
|
|
733
|
+
} : {},
|
|
734
|
+
...o.resourceDocumentation ? {
|
|
735
|
+
resource_documentation: o.resourceDocumentation
|
|
736
|
+
} : {},
|
|
737
|
+
...o.metadata
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
/** HTTP handler for the metadata paths. Public and CORS-enabled so browser-based clients can read it. */
|
|
741
|
+
async handleMetadataHttp(req) {
|
|
742
|
+
const cors = {
|
|
743
|
+
"access-control-allow-origin": "*",
|
|
744
|
+
"access-control-allow-methods": "GET, OPTIONS"
|
|
745
|
+
};
|
|
746
|
+
const method = req.method.toUpperCase();
|
|
747
|
+
if (!this.options.oauth) return {
|
|
748
|
+
status: 404,
|
|
749
|
+
headers: {}
|
|
750
|
+
};
|
|
751
|
+
if (method === "OPTIONS") return {
|
|
752
|
+
status: 204,
|
|
753
|
+
headers: {
|
|
754
|
+
...cors,
|
|
755
|
+
"access-control-allow-headers": "*"
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
if (method !== "GET" && method !== "HEAD") return {
|
|
759
|
+
status: 405,
|
|
760
|
+
headers: {
|
|
761
|
+
allow: "GET, OPTIONS"
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
const body = JSON.stringify(await this.protectedResourceMetadata());
|
|
765
|
+
return {
|
|
766
|
+
status: 200,
|
|
767
|
+
headers: {
|
|
768
|
+
...cors,
|
|
769
|
+
"content-type": "application/json",
|
|
770
|
+
"cache-control": "public, max-age=3600"
|
|
771
|
+
},
|
|
772
|
+
body: method === "HEAD" ? void 0 : body
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
592
776
|
* Framework-neutral Streamable HTTP handler. Adapters convert their request
|
|
593
777
|
* into McpHttpRequest, call this, and write the McpHttpResponse back.
|
|
594
778
|
*/
|
|
@@ -603,6 +787,15 @@ var McpServer = class {
|
|
|
603
787
|
body: JSON.stringify(fail(null, INVALID_REQUEST, "Origin not allowed"))
|
|
604
788
|
};
|
|
605
789
|
}
|
|
790
|
+
const oauth = this.options.oauth;
|
|
791
|
+
if (oauth) {
|
|
792
|
+
const outcome = await authenticate(oauth, req.headers);
|
|
793
|
+
if (!outcome.ok) return authError(outcome);
|
|
794
|
+
ctx = {
|
|
795
|
+
...ctx,
|
|
796
|
+
auth: outcome.auth
|
|
797
|
+
};
|
|
798
|
+
}
|
|
606
799
|
if (req.method.toUpperCase() !== "POST") {
|
|
607
800
|
return {
|
|
608
801
|
status: 405,
|
|
@@ -623,6 +816,27 @@ var McpServer = class {
|
|
|
623
816
|
};
|
|
624
817
|
}
|
|
625
818
|
}
|
|
819
|
+
if (oauth) {
|
|
820
|
+
await this.load();
|
|
821
|
+
const calls = Array.isArray(body) ? body : [
|
|
822
|
+
body
|
|
823
|
+
];
|
|
824
|
+
for (const m of calls) {
|
|
825
|
+
if (m?.method !== "tools/call") continue;
|
|
826
|
+
const tool = this.tools.get(m.params?.name);
|
|
827
|
+
const missing = missingScopes(ctx.auth, tool?.scopes);
|
|
828
|
+
if (missing.length) {
|
|
829
|
+
const scope = [
|
|
830
|
+
.../* @__PURE__ */ new Set([
|
|
831
|
+
...ctx.auth?.scopes ?? [],
|
|
832
|
+
...oauth.requiredScopes ?? [],
|
|
833
|
+
...tool?.scopes ?? []
|
|
834
|
+
])
|
|
835
|
+
];
|
|
836
|
+
return authError(deny(oauth, 403, "insufficient_scope", `Tool "${tool.name}" needs scope: ${missing.join(" ")}`, scope));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
626
840
|
const response = await this.handleMessage(body, ctx);
|
|
627
841
|
if (response === null) return {
|
|
628
842
|
status: 202,
|
|
@@ -635,6 +849,20 @@ var McpServer = class {
|
|
|
635
849
|
};
|
|
636
850
|
}
|
|
637
851
|
};
|
|
852
|
+
function authError(o) {
|
|
853
|
+
return {
|
|
854
|
+
status: o.status,
|
|
855
|
+
headers: {
|
|
856
|
+
"content-type": "application/json",
|
|
857
|
+
"www-authenticate": o.challenge
|
|
858
|
+
},
|
|
859
|
+
body: JSON.stringify({
|
|
860
|
+
error: o.error,
|
|
861
|
+
error_description: o.description
|
|
862
|
+
})
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
__name(authError, "authError");
|
|
638
866
|
|
|
639
867
|
// src/nestjs/dto-schema.ts
|
|
640
868
|
var PRIMITIVES = /* @__PURE__ */ new Map([
|
|
@@ -843,11 +1071,13 @@ var McpService = class {
|
|
|
843
1071
|
discovery;
|
|
844
1072
|
appConfig;
|
|
845
1073
|
options;
|
|
1074
|
+
adapterHost;
|
|
846
1075
|
server;
|
|
847
|
-
constructor(discovery, appConfig, options) {
|
|
1076
|
+
constructor(discovery, appConfig, options, adapterHost) {
|
|
848
1077
|
this.discovery = discovery;
|
|
849
1078
|
this.appConfig = appConfig;
|
|
850
1079
|
this.options = options;
|
|
1080
|
+
this.adapterHost = adapterHost;
|
|
851
1081
|
this.server = new McpServer(options);
|
|
852
1082
|
for (const t of options.tools ?? []) this.server.addTool(t);
|
|
853
1083
|
const dispatch = createFetchDispatcher({
|
|
@@ -860,6 +1090,26 @@ var McpService = class {
|
|
|
860
1090
|
}
|
|
861
1091
|
});
|
|
862
1092
|
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Serve the OAuth Protected Resource Metadata at the root of the app, outside
|
|
1095
|
+
* the global prefix and guards (it must be public). Runs before Nest registers
|
|
1096
|
+
* its own routes.
|
|
1097
|
+
*/
|
|
1098
|
+
onModuleInit() {
|
|
1099
|
+
const adapter = this.adapterHost?.httpAdapter;
|
|
1100
|
+
if (!adapter) return;
|
|
1101
|
+
for (const path of this.server.oauthMetadataPaths) {
|
|
1102
|
+
const handler = /* @__PURE__ */ __name((method) => async (_req, res) => {
|
|
1103
|
+
const out = await this.server.handleMetadataHttp({
|
|
1104
|
+
method
|
|
1105
|
+
});
|
|
1106
|
+
for (const [k, v] of Object.entries(out.headers)) adapter.setHeader(res, k, v);
|
|
1107
|
+
adapter.reply(res, out.body, out.status);
|
|
1108
|
+
}, "handler");
|
|
1109
|
+
adapter.get(path, handler("GET"));
|
|
1110
|
+
adapter.options(path, handler("OPTIONS"));
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
863
1113
|
globalPrefix() {
|
|
864
1114
|
return this.appConfig.getGlobalPrefix?.() ?? "";
|
|
865
1115
|
}
|
|
@@ -992,6 +1242,7 @@ __name(toSnake, "toSnake");
|
|
|
992
1242
|
(0, import_common.Inject)(import_core.DiscoveryService)(McpService, void 0, 0);
|
|
993
1243
|
(0, import_common.Inject)(import_core.ApplicationConfig)(McpService, void 0, 1);
|
|
994
1244
|
(0, import_common.Inject)(MCP_MODULE_OPTIONS)(McpService, void 0, 2);
|
|
1245
|
+
(0, import_common.Inject)(import_core.HttpAdapterHost)(McpService, void 0, 3);
|
|
995
1246
|
function createMcpController(options) {
|
|
996
1247
|
let McpController = class McpController {
|
|
997
1248
|
static {
|
|
@@ -1034,6 +1285,7 @@ function createMcpController(options) {
|
|
|
1034
1285
|
(0, import_common.Req)()(proto, "handle", 0);
|
|
1035
1286
|
(0, import_common.Res)()(proto, "handle", 1);
|
|
1036
1287
|
if (options.guards?.length) (0, import_common.UseGuards)(...options.guards)(McpController);
|
|
1288
|
+
for (const decorate of options.decorators ?? []) decorate(McpController);
|
|
1037
1289
|
return McpController;
|
|
1038
1290
|
}
|
|
1039
1291
|
__name(createMcpController, "createMcpController");
|