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
@@ -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");