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.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) {
|
|
@@ -336,6 +336,7 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
|
|
|
336
336
|
...defaultAnnotations(method),
|
|
337
337
|
...opts.annotations
|
|
338
338
|
},
|
|
339
|
+
scopes: opts.scopes,
|
|
339
340
|
validate: toValidator(opts.input),
|
|
340
341
|
async handler(args, ctx) {
|
|
341
342
|
const missing = opts.input ? void 0 : checkRequired(schema, args);
|
|
@@ -384,6 +385,115 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
|
|
|
384
385
|
}
|
|
385
386
|
__name(createRouteTool, "createRouteTool");
|
|
386
387
|
|
|
388
|
+
// src/core/oauth.ts
|
|
389
|
+
var PROTECTED_RESOURCE_WELL_KNOWN = "/.well-known/oauth-protected-resource";
|
|
390
|
+
function protectedResourceMetadataPaths(resource) {
|
|
391
|
+
const pathname = new URL(resource).pathname.replace(/\/+$/, "");
|
|
392
|
+
return pathname ? [
|
|
393
|
+
PROTECTED_RESOURCE_WELL_KNOWN + pathname,
|
|
394
|
+
PROTECTED_RESOURCE_WELL_KNOWN
|
|
395
|
+
] : [
|
|
396
|
+
PROTECTED_RESOURCE_WELL_KNOWN
|
|
397
|
+
];
|
|
398
|
+
}
|
|
399
|
+
__name(protectedResourceMetadataPaths, "protectedResourceMetadataPaths");
|
|
400
|
+
function protectedResourceMetadataUrl(resource) {
|
|
401
|
+
return new URL(protectedResourceMetadataPaths(resource)[0], resource).href;
|
|
402
|
+
}
|
|
403
|
+
__name(protectedResourceMetadataUrl, "protectedResourceMetadataUrl");
|
|
404
|
+
function validateOAuthOptions(o) {
|
|
405
|
+
let url;
|
|
406
|
+
try {
|
|
407
|
+
url = new URL(o.resource);
|
|
408
|
+
} catch {
|
|
409
|
+
throw new Error(`mcp-expose: oauth.resource must be an absolute URL, got ${JSON.stringify(o.resource)}`);
|
|
410
|
+
}
|
|
411
|
+
if (url.hash) throw new Error("mcp-expose: oauth.resource must not contain a fragment");
|
|
412
|
+
if (!o.authorizationServers?.length) throw new Error("mcp-expose: oauth.authorizationServers must not be empty");
|
|
413
|
+
if (typeof o.verifyToken !== "function") throw new Error("mcp-expose: oauth.verifyToken is required");
|
|
414
|
+
}
|
|
415
|
+
__name(validateOAuthOptions, "validateOAuthOptions");
|
|
416
|
+
var quote = /* @__PURE__ */ __name((v) => `"${v.replace(/["\\]/g, "\\$&")}"`, "quote");
|
|
417
|
+
function bearerChallenge(resource, params = {}) {
|
|
418
|
+
const parts = [];
|
|
419
|
+
if (params.error) parts.push(`error=${quote(params.error)}`);
|
|
420
|
+
if (params.errorDescription) parts.push(`error_description=${quote(params.errorDescription)}`);
|
|
421
|
+
if (params.scope?.length) parts.push(`scope=${quote(params.scope.join(" "))}`);
|
|
422
|
+
parts.push(`resource_metadata=${quote(protectedResourceMetadataUrl(resource))}`);
|
|
423
|
+
return `Bearer ${parts.join(", ")}`;
|
|
424
|
+
}
|
|
425
|
+
__name(bearerChallenge, "bearerChallenge");
|
|
426
|
+
function bearerToken(headers) {
|
|
427
|
+
const m = /^Bearer[ ]+([^\s,]+)\s*$/i.exec(headers.authorization ?? "");
|
|
428
|
+
return m?.[1];
|
|
429
|
+
}
|
|
430
|
+
__name(bearerToken, "bearerToken");
|
|
431
|
+
async function authenticate(o, headers) {
|
|
432
|
+
const token = bearerToken(headers);
|
|
433
|
+
if (!token) {
|
|
434
|
+
return {
|
|
435
|
+
ok: false,
|
|
436
|
+
status: 401,
|
|
437
|
+
error: "unauthorized",
|
|
438
|
+
description: "Authorization required",
|
|
439
|
+
challenge: bearerChallenge(o.resource, {
|
|
440
|
+
scope: o.requiredScopes
|
|
441
|
+
})
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
let auth;
|
|
445
|
+
try {
|
|
446
|
+
auth = await o.verifyToken(token, {
|
|
447
|
+
resource: o.resource,
|
|
448
|
+
headers
|
|
449
|
+
});
|
|
450
|
+
} catch {
|
|
451
|
+
auth = void 0;
|
|
452
|
+
}
|
|
453
|
+
if (!auth) return deny(o, 401, "invalid_token", "The access token is invalid");
|
|
454
|
+
if (auth.expiresAt !== void 0 && auth.expiresAt * 1e3 <= Date.now()) {
|
|
455
|
+
return deny(o, 401, "invalid_token", "The access token has expired");
|
|
456
|
+
}
|
|
457
|
+
const missing = missingScopes(auth, o.requiredScopes);
|
|
458
|
+
if (missing.length) {
|
|
459
|
+
const scope = [
|
|
460
|
+
.../* @__PURE__ */ new Set([
|
|
461
|
+
...auth.scopes ?? [],
|
|
462
|
+
...o.requiredScopes ?? []
|
|
463
|
+
])
|
|
464
|
+
];
|
|
465
|
+
return deny(o, 403, "insufficient_scope", `Missing scope: ${missing.join(" ")}`, scope);
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
ok: true,
|
|
469
|
+
auth: {
|
|
470
|
+
...auth,
|
|
471
|
+
scopes: auth.scopes ?? []
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
__name(authenticate, "authenticate");
|
|
476
|
+
function deny(o, status, error, description, scope) {
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
status,
|
|
480
|
+
error,
|
|
481
|
+
description,
|
|
482
|
+
challenge: bearerChallenge(o.resource, {
|
|
483
|
+
error,
|
|
484
|
+
errorDescription: description,
|
|
485
|
+
scope
|
|
486
|
+
})
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
__name(deny, "deny");
|
|
490
|
+
function missingScopes(auth, required) {
|
|
491
|
+
if (!required?.length) return [];
|
|
492
|
+
const granted = new Set(auth?.scopes ?? []);
|
|
493
|
+
return required.filter((s) => !granted.has(s));
|
|
494
|
+
}
|
|
495
|
+
__name(missingScopes, "missingScopes");
|
|
496
|
+
|
|
387
497
|
// src/core/server.ts
|
|
388
498
|
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
389
499
|
"2025-11-25",
|
|
@@ -418,6 +528,7 @@ var McpServer = class {
|
|
|
418
528
|
loaders = [];
|
|
419
529
|
constructor(options) {
|
|
420
530
|
this.options = options;
|
|
531
|
+
if (options.oauth) validateOAuthOptions(options.oauth);
|
|
421
532
|
}
|
|
422
533
|
/** Register a tool. Throws on duplicate names. */
|
|
423
534
|
addTool(tool) {
|
|
@@ -552,6 +663,79 @@ var McpServer = class {
|
|
|
552
663
|
return (allowed ?? []).includes(origin);
|
|
553
664
|
}
|
|
554
665
|
/**
|
|
666
|
+
* Paths on which adapters serve the OAuth Protected Resource Metadata
|
|
667
|
+
* (empty when `oauth` is not configured). They must be public: no auth middleware.
|
|
668
|
+
*/
|
|
669
|
+
get oauthMetadataPaths() {
|
|
670
|
+
return this.options.oauth ? protectedResourceMetadataPaths(this.options.oauth.resource) : [];
|
|
671
|
+
}
|
|
672
|
+
/** The OAuth Protected Resource Metadata document (RFC 9728). */
|
|
673
|
+
async protectedResourceMetadata() {
|
|
674
|
+
const o = this.options.oauth;
|
|
675
|
+
if (!o) throw new Error("mcp-expose: the oauth option is not configured");
|
|
676
|
+
await this.load();
|
|
677
|
+
const scopes = o.scopesSupported ?? [
|
|
678
|
+
.../* @__PURE__ */ new Set([
|
|
679
|
+
...o.requiredScopes ?? [],
|
|
680
|
+
...[
|
|
681
|
+
...this.tools.values()
|
|
682
|
+
].flatMap((t) => t.scopes ?? [])
|
|
683
|
+
])
|
|
684
|
+
];
|
|
685
|
+
return {
|
|
686
|
+
resource: o.resource,
|
|
687
|
+
authorization_servers: o.authorizationServers,
|
|
688
|
+
...scopes.length ? {
|
|
689
|
+
scopes_supported: scopes
|
|
690
|
+
} : {},
|
|
691
|
+
bearer_methods_supported: [
|
|
692
|
+
"header"
|
|
693
|
+
],
|
|
694
|
+
...o.resourceName ? {
|
|
695
|
+
resource_name: o.resourceName
|
|
696
|
+
} : {},
|
|
697
|
+
...o.resourceDocumentation ? {
|
|
698
|
+
resource_documentation: o.resourceDocumentation
|
|
699
|
+
} : {},
|
|
700
|
+
...o.metadata
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
/** HTTP handler for the metadata paths. Public and CORS-enabled so browser-based clients can read it. */
|
|
704
|
+
async handleMetadataHttp(req) {
|
|
705
|
+
const cors = {
|
|
706
|
+
"access-control-allow-origin": "*",
|
|
707
|
+
"access-control-allow-methods": "GET, OPTIONS"
|
|
708
|
+
};
|
|
709
|
+
const method = req.method.toUpperCase();
|
|
710
|
+
if (!this.options.oauth) return {
|
|
711
|
+
status: 404,
|
|
712
|
+
headers: {}
|
|
713
|
+
};
|
|
714
|
+
if (method === "OPTIONS") return {
|
|
715
|
+
status: 204,
|
|
716
|
+
headers: {
|
|
717
|
+
...cors,
|
|
718
|
+
"access-control-allow-headers": "*"
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
if (method !== "GET" && method !== "HEAD") return {
|
|
722
|
+
status: 405,
|
|
723
|
+
headers: {
|
|
724
|
+
allow: "GET, OPTIONS"
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
const body = JSON.stringify(await this.protectedResourceMetadata());
|
|
728
|
+
return {
|
|
729
|
+
status: 200,
|
|
730
|
+
headers: {
|
|
731
|
+
...cors,
|
|
732
|
+
"content-type": "application/json",
|
|
733
|
+
"cache-control": "public, max-age=3600"
|
|
734
|
+
},
|
|
735
|
+
body: method === "HEAD" ? void 0 : body
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
555
739
|
* Framework-neutral Streamable HTTP handler. Adapters convert their request
|
|
556
740
|
* into McpHttpRequest, call this, and write the McpHttpResponse back.
|
|
557
741
|
*/
|
|
@@ -566,6 +750,15 @@ var McpServer = class {
|
|
|
566
750
|
body: JSON.stringify(fail(null, INVALID_REQUEST, "Origin not allowed"))
|
|
567
751
|
};
|
|
568
752
|
}
|
|
753
|
+
const oauth = this.options.oauth;
|
|
754
|
+
if (oauth) {
|
|
755
|
+
const outcome = await authenticate(oauth, req.headers);
|
|
756
|
+
if (!outcome.ok) return authError(outcome);
|
|
757
|
+
ctx = {
|
|
758
|
+
...ctx,
|
|
759
|
+
auth: outcome.auth
|
|
760
|
+
};
|
|
761
|
+
}
|
|
569
762
|
if (req.method.toUpperCase() !== "POST") {
|
|
570
763
|
return {
|
|
571
764
|
status: 405,
|
|
@@ -586,6 +779,27 @@ var McpServer = class {
|
|
|
586
779
|
};
|
|
587
780
|
}
|
|
588
781
|
}
|
|
782
|
+
if (oauth) {
|
|
783
|
+
await this.load();
|
|
784
|
+
const calls = Array.isArray(body) ? body : [
|
|
785
|
+
body
|
|
786
|
+
];
|
|
787
|
+
for (const m of calls) {
|
|
788
|
+
if (m?.method !== "tools/call") continue;
|
|
789
|
+
const tool = this.tools.get(m.params?.name);
|
|
790
|
+
const missing = missingScopes(ctx.auth, tool?.scopes);
|
|
791
|
+
if (missing.length) {
|
|
792
|
+
const scope = [
|
|
793
|
+
.../* @__PURE__ */ new Set([
|
|
794
|
+
...ctx.auth?.scopes ?? [],
|
|
795
|
+
...oauth.requiredScopes ?? [],
|
|
796
|
+
...tool?.scopes ?? []
|
|
797
|
+
])
|
|
798
|
+
];
|
|
799
|
+
return authError(deny(oauth, 403, "insufficient_scope", `Tool "${tool.name}" needs scope: ${missing.join(" ")}`, scope));
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
589
803
|
const response = await this.handleMessage(body, ctx);
|
|
590
804
|
if (response === null) return {
|
|
591
805
|
status: 202,
|
|
@@ -598,6 +812,20 @@ var McpServer = class {
|
|
|
598
812
|
};
|
|
599
813
|
}
|
|
600
814
|
};
|
|
815
|
+
function authError(o) {
|
|
816
|
+
return {
|
|
817
|
+
status: o.status,
|
|
818
|
+
headers: {
|
|
819
|
+
"content-type": "application/json",
|
|
820
|
+
"www-authenticate": o.challenge
|
|
821
|
+
},
|
|
822
|
+
body: JSON.stringify({
|
|
823
|
+
error: o.error,
|
|
824
|
+
error_description: o.description
|
|
825
|
+
})
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
__name(authError, "authError");
|
|
601
829
|
|
|
602
830
|
// src/nestjs/dto-schema.ts
|
|
603
831
|
var PRIMITIVES = /* @__PURE__ */ new Map([
|
|
@@ -806,11 +1034,13 @@ var McpService = class {
|
|
|
806
1034
|
discovery;
|
|
807
1035
|
appConfig;
|
|
808
1036
|
options;
|
|
1037
|
+
adapterHost;
|
|
809
1038
|
server;
|
|
810
|
-
constructor(discovery, appConfig, options) {
|
|
1039
|
+
constructor(discovery, appConfig, options, adapterHost) {
|
|
811
1040
|
this.discovery = discovery;
|
|
812
1041
|
this.appConfig = appConfig;
|
|
813
1042
|
this.options = options;
|
|
1043
|
+
this.adapterHost = adapterHost;
|
|
814
1044
|
this.server = new McpServer(options);
|
|
815
1045
|
for (const t of options.tools ?? []) this.server.addTool(t);
|
|
816
1046
|
const dispatch = createFetchDispatcher({
|
|
@@ -823,6 +1053,26 @@ var McpService = class {
|
|
|
823
1053
|
}
|
|
824
1054
|
});
|
|
825
1055
|
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Serve the OAuth Protected Resource Metadata at the root of the app, outside
|
|
1058
|
+
* the global prefix and guards (it must be public). Runs before Nest registers
|
|
1059
|
+
* its own routes.
|
|
1060
|
+
*/
|
|
1061
|
+
onModuleInit() {
|
|
1062
|
+
const adapter = this.adapterHost?.httpAdapter;
|
|
1063
|
+
if (!adapter) return;
|
|
1064
|
+
for (const path of this.server.oauthMetadataPaths) {
|
|
1065
|
+
const handler = /* @__PURE__ */ __name((method) => async (_req, res) => {
|
|
1066
|
+
const out = await this.server.handleMetadataHttp({
|
|
1067
|
+
method
|
|
1068
|
+
});
|
|
1069
|
+
for (const [k, v] of Object.entries(out.headers)) adapter.setHeader(res, k, v);
|
|
1070
|
+
adapter.reply(res, out.body, out.status);
|
|
1071
|
+
}, "handler");
|
|
1072
|
+
adapter.get(path, handler("GET"));
|
|
1073
|
+
adapter.options(path, handler("OPTIONS"));
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
826
1076
|
globalPrefix() {
|
|
827
1077
|
return this.appConfig.getGlobalPrefix?.() ?? "";
|
|
828
1078
|
}
|
|
@@ -955,6 +1205,7 @@ Injectable()(McpService);
|
|
|
955
1205
|
Inject(DiscoveryService)(McpService, void 0, 0);
|
|
956
1206
|
Inject(ApplicationConfig)(McpService, void 0, 1);
|
|
957
1207
|
Inject(MCP_MODULE_OPTIONS)(McpService, void 0, 2);
|
|
1208
|
+
Inject(HttpAdapterHost)(McpService, void 0, 3);
|
|
958
1209
|
function createMcpController(options) {
|
|
959
1210
|
let McpController = class McpController {
|
|
960
1211
|
static {
|
|
@@ -997,6 +1248,7 @@ function createMcpController(options) {
|
|
|
997
1248
|
Req()(proto, "handle", 0);
|
|
998
1249
|
Res()(proto, "handle", 1);
|
|
999
1250
|
if (options.guards?.length) UseGuards(...options.guards)(McpController);
|
|
1251
|
+
for (const decorate of options.decorators ?? []) decorate(McpController);
|
|
1000
1252
|
return McpController;
|
|
1001
1253
|
}
|
|
1002
1254
|
__name(createMcpController, "createMcpController");
|