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.
Files changed (55) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +181 -32
  3. package/dist/adonisjs/index.cjs +366 -6
  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 +366 -6
  8. package/dist/adonisjs/index.js.map +1 -1
  9. package/dist/express/index.cjs +364 -7
  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 +364 -7
  14. package/dist/express/index.js.map +1 -1
  15. package/dist/fastify/index.cjs +373 -6
  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 +373 -6
  20. package/dist/fastify/index.js.map +1 -1
  21. package/dist/hono/index.cjs +367 -6
  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 +367 -6
  26. package/dist/hono/index.js.map +1 -1
  27. package/dist/index.cjs +371 -31
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +10 -5
  30. package/dist/index.d.ts +10 -5
  31. package/dist/index.js +367 -31
  32. package/dist/index.js.map +1 -1
  33. package/dist/koa/index.cjs +366 -6
  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 +366 -6
  38. package/dist/koa/index.js.map +1 -1
  39. package/dist/nestjs/index.cjs +381 -7
  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 +382 -8
  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--mQVQq3A.d.cts} +81 -1
  54. package/dist/{server-fzkb-wJo.d.cts → server-BPDTsT2o.d.ts} +81 -1
  55. package/package.json +22 -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) {
@@ -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
- let text = res.body;
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 res = await dispatch({
458
+ const request = {
375
459
  method,
376
460
  path,
377
461
  query,
378
462
  body,
379
463
  headers
380
- }, ctx);
381
- return responseToResult(res, maxChars);
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");