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.
Files changed (55) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +102 -13
  3. package/dist/adonisjs/index.cjs +238 -0
  4. package/dist/adonisjs/index.cjs.map +1 -1
  5. package/dist/adonisjs/index.d.cts +2 -1
  6. package/dist/adonisjs/index.d.ts +2 -1
  7. package/dist/adonisjs/index.js +238 -0
  8. package/dist/adonisjs/index.js.map +1 -1
  9. package/dist/express/index.cjs +236 -1
  10. package/dist/express/index.cjs.map +1 -1
  11. package/dist/express/index.d.cts +4 -2
  12. package/dist/express/index.d.ts +4 -2
  13. package/dist/express/index.js +236 -1
  14. package/dist/express/index.js.map +1 -1
  15. package/dist/fastify/index.cjs +245 -0
  16. package/dist/fastify/index.cjs.map +1 -1
  17. package/dist/fastify/index.d.cts +2 -1
  18. package/dist/fastify/index.d.ts +2 -1
  19. package/dist/fastify/index.js +245 -0
  20. package/dist/fastify/index.js.map +1 -1
  21. package/dist/hono/index.cjs +239 -0
  22. package/dist/hono/index.cjs.map +1 -1
  23. package/dist/hono/index.d.cts +2 -1
  24. package/dist/hono/index.d.ts +2 -1
  25. package/dist/hono/index.js +239 -0
  26. package/dist/hono/index.js.map +1 -1
  27. package/dist/index.cjs +243 -0
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +5 -2
  30. package/dist/index.d.ts +5 -2
  31. package/dist/index.js +239 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/koa/index.cjs +238 -0
  34. package/dist/koa/index.cjs.map +1 -1
  35. package/dist/koa/index.d.cts +2 -1
  36. package/dist/koa/index.d.ts +2 -1
  37. package/dist/koa/index.js +238 -0
  38. package/dist/koa/index.js.map +1 -1
  39. package/dist/nestjs/index.cjs +253 -1
  40. package/dist/nestjs/index.cjs.map +1 -1
  41. package/dist/nestjs/index.d.cts +21 -7
  42. package/dist/nestjs/index.d.ts +21 -7
  43. package/dist/nestjs/index.js +254 -2
  44. package/dist/nestjs/index.js.map +1 -1
  45. package/dist/oauth/index.cjs +214 -0
  46. package/dist/oauth/index.cjs.map +1 -0
  47. package/dist/oauth/index.d.cts +69 -0
  48. package/dist/oauth/index.d.ts +69 -0
  49. package/dist/oauth/index.js +177 -0
  50. package/dist/oauth/index.js.map +1 -0
  51. package/dist/oauth-8RmQamGP.d.cts +75 -0
  52. package/dist/oauth-8RmQamGP.d.ts +75 -0
  53. package/dist/{server-fzkb-wJo.d.ts → server-BFQ_Hdh9.d.cts} +27 -0
  54. package/dist/{server-fzkb-wJo.d.cts → server-BRw1E2xy.d.ts} +27 -0
  55. package/package.json +21 -2
@@ -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");