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
@@ -118,6 +118,38 @@ function writeNodeResponse(res, out) {
118
118
  }
119
119
  __name(writeNodeResponse, "writeNodeResponse");
120
120
 
121
+ // src/core/result.ts
122
+ function isToolResult(value) {
123
+ const content = value?.content;
124
+ return !!value && typeof value === "object" && Array.isArray(content) && content.length > 0 && content.every((c) => !!c && typeof c === "object" && typeof c.type === "string");
125
+ }
126
+ __name(isToolResult, "isToolResult");
127
+ function toToolResult(value) {
128
+ if (isToolResult(value)) return value;
129
+ if (typeof value === "string") return {
130
+ content: [
131
+ {
132
+ type: "text",
133
+ text: value
134
+ }
135
+ ]
136
+ };
137
+ const text = JSON.stringify(value ?? null);
138
+ const result = {
139
+ content: [
140
+ {
141
+ type: "text",
142
+ text
143
+ }
144
+ ]
145
+ };
146
+ if (value && typeof value === "object" && !Array.isArray(value)) {
147
+ result.structuredContent = value;
148
+ }
149
+ return result;
150
+ }
151
+ __name(toToolResult, "toToolResult");
152
+
121
153
  // src/core/schema.ts
122
154
  function isStandard(s) {
123
155
  return typeof s === "object" && s !== null && "~standard" in s;
@@ -343,9 +375,7 @@ function splitArgs(args, mapping) {
343
375
  }
344
376
  __name(splitArgs, "splitArgs");
345
377
  function responseToResult(res, maxChars) {
346
- let text = res.body;
347
- if (text.length > maxChars) text = `${text.slice(0, maxChars)}
348
- \u2026[truncated ${text.length - maxChars} chars]`;
378
+ const text = truncate(res.body, maxChars);
349
379
  const isError = res.status >= 400;
350
380
  const result = {
351
381
  content: [
@@ -370,6 +400,58 @@ ${text}` : text || `HTTP ${res.status}`
370
400
  return result;
371
401
  }
372
402
  __name(responseToResult, "responseToResult");
403
+ function safeDecode(segment) {
404
+ try {
405
+ return decodeURIComponent(segment);
406
+ } catch {
407
+ return segment;
408
+ }
409
+ }
410
+ __name(safeDecode, "safeDecode");
411
+ function truncate(text, maxChars) {
412
+ return text.length > maxChars ? `${text.slice(0, maxChars)}
413
+ \u2026[truncated ${text.length - maxChars} chars]` : text;
414
+ }
415
+ __name(truncate, "truncate");
416
+ function toRouteToolResponse(res) {
417
+ let json;
418
+ if ((res.headers["content-type"] ?? "").includes("json") && res.body) {
419
+ try {
420
+ json = JSON.parse(res.body);
421
+ } catch {
422
+ }
423
+ }
424
+ return {
425
+ ...res,
426
+ json,
427
+ ok: res.status < 400
428
+ };
429
+ }
430
+ __name(toRouteToolResponse, "toRouteToolResponse");
431
+ function transformedResult(value, res, maxChars) {
432
+ const explicit = isToolResult(value);
433
+ const result = toToolResult(value);
434
+ if (!explicit && !res.ok) {
435
+ result.isError = true;
436
+ delete result.structuredContent;
437
+ }
438
+ let truncated = false;
439
+ const content = result.content.map((c) => {
440
+ if (c.type !== "text" || c.text.length <= maxChars) return c;
441
+ truncated = true;
442
+ return {
443
+ ...c,
444
+ text: truncate(c.text, maxChars)
445
+ };
446
+ });
447
+ if (!truncated) return result;
448
+ const { structuredContent: _dropped, ...rest } = result;
449
+ return {
450
+ ...rest,
451
+ content
452
+ };
453
+ }
454
+ __name(transformedResult, "transformedResult");
373
455
  function pickForwardHeaders(ctx, names) {
374
456
  const out = {};
375
457
  for (const name of names) {
@@ -385,6 +467,7 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
385
467
  const { schema, mapping } = buildInputSchema(route, opts);
386
468
  const forward = serverOpts.forwardHeaders ?? DEFAULT_FORWARD_HEADERS;
387
469
  const maxChars = serverOpts.maxResponseChars ?? 1e5;
470
+ const transform = opts.transformResponse ?? serverOpts.transformResponse;
388
471
  return {
389
472
  name,
390
473
  title: opts.title,
@@ -394,6 +477,7 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
394
477
  ...defaultAnnotations(method),
395
478
  ...opts.annotations
396
479
  },
480
+ scopes: opts.scopes,
397
481
  validate: toValidator(opts.input),
398
482
  async handler(args, ctx) {
399
483
  const missing = opts.input ? void 0 : checkRequired(schema, args);
@@ -429,19 +513,167 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
429
513
  ]
430
514
  };
431
515
  }
432
- const res = await dispatch({
516
+ const request = {
433
517
  method,
434
518
  path,
435
519
  query,
436
520
  body,
437
521
  headers
438
- }, ctx);
439
- return responseToResult(res, maxChars);
522
+ };
523
+ const res = await dispatch(request, ctx);
524
+ if (!transform) return responseToResult(res, maxChars);
525
+ const { "content-type": _ct, ...baseHeaders } = headers;
526
+ const hookCtx = {
527
+ tool: name,
528
+ args,
529
+ request,
530
+ toolContext: ctx,
531
+ defaultResult: /* @__PURE__ */ __name(() => responseToResult(res, maxChars), "defaultResult"),
532
+ async callRoute(r) {
533
+ if (!r.path.startsWith("/")) {
534
+ throw new Error(`callRoute: path must start with "/", got ${JSON.stringify(r.path)}`);
535
+ }
536
+ const params = r.params ?? {};
537
+ const missing2 = pathParams(r.path).filter((p) => params[p] === void 0 && !r.path.includes(`:${p}?`));
538
+ if (missing2.length) throw new Error(`callRoute: missing params for ${r.path}: ${missing2.join(", ")}`);
539
+ const subPath = interpolatePath(r.path, params);
540
+ if (subPath.split("/").some((seg) => [
541
+ ".",
542
+ ".."
543
+ ].includes(safeDecode(seg)))) {
544
+ throw new Error(`callRoute: path must not contain "." or ".." segments: ${JSON.stringify(subPath)}`);
545
+ }
546
+ const sub = {
547
+ method: (r.method ?? "GET").toUpperCase(),
548
+ path: subPath,
549
+ query: r.query ?? {},
550
+ body: r.body,
551
+ headers: {
552
+ ...baseHeaders,
553
+ ...r.headers
554
+ }
555
+ };
556
+ if (sub.body !== void 0) sub.headers["content-type"] = "application/json";
557
+ return toRouteToolResponse(await dispatch(sub, ctx));
558
+ }
559
+ };
560
+ const response = toRouteToolResponse(res);
561
+ const value = await transform(response, hookCtx);
562
+ return value === void 0 ? responseToResult(res, maxChars) : transformedResult(value, response, maxChars);
440
563
  }
441
564
  };
442
565
  }
443
566
  __name(createRouteTool, "createRouteTool");
444
567
 
568
+ // src/core/oauth.ts
569
+ var PROTECTED_RESOURCE_WELL_KNOWN = "/.well-known/oauth-protected-resource";
570
+ function protectedResourceMetadataPaths(resource) {
571
+ const pathname = new URL(resource).pathname.replace(/\/+$/, "");
572
+ return pathname ? [
573
+ PROTECTED_RESOURCE_WELL_KNOWN + pathname,
574
+ PROTECTED_RESOURCE_WELL_KNOWN
575
+ ] : [
576
+ PROTECTED_RESOURCE_WELL_KNOWN
577
+ ];
578
+ }
579
+ __name(protectedResourceMetadataPaths, "protectedResourceMetadataPaths");
580
+ function protectedResourceMetadataUrl(resource) {
581
+ return new URL(protectedResourceMetadataPaths(resource)[0], resource).href;
582
+ }
583
+ __name(protectedResourceMetadataUrl, "protectedResourceMetadataUrl");
584
+ function validateOAuthOptions(o) {
585
+ let url;
586
+ try {
587
+ url = new URL(o.resource);
588
+ } catch {
589
+ throw new Error(`mcp-expose: oauth.resource must be an absolute URL, got ${JSON.stringify(o.resource)}`);
590
+ }
591
+ if (url.hash) throw new Error("mcp-expose: oauth.resource must not contain a fragment");
592
+ if (!o.authorizationServers?.length) throw new Error("mcp-expose: oauth.authorizationServers must not be empty");
593
+ if (typeof o.verifyToken !== "function") throw new Error("mcp-expose: oauth.verifyToken is required");
594
+ }
595
+ __name(validateOAuthOptions, "validateOAuthOptions");
596
+ var quote = /* @__PURE__ */ __name((v) => `"${v.replace(/["\\]/g, "\\$&")}"`, "quote");
597
+ function bearerChallenge(resource, params = {}) {
598
+ const parts = [];
599
+ if (params.error) parts.push(`error=${quote(params.error)}`);
600
+ if (params.errorDescription) parts.push(`error_description=${quote(params.errorDescription)}`);
601
+ if (params.scope?.length) parts.push(`scope=${quote(params.scope.join(" "))}`);
602
+ parts.push(`resource_metadata=${quote(protectedResourceMetadataUrl(resource))}`);
603
+ return `Bearer ${parts.join(", ")}`;
604
+ }
605
+ __name(bearerChallenge, "bearerChallenge");
606
+ function bearerToken(headers) {
607
+ const m = /^Bearer[ ]+([^\s,]+)\s*$/i.exec(headers.authorization ?? "");
608
+ return m?.[1];
609
+ }
610
+ __name(bearerToken, "bearerToken");
611
+ async function authenticate(o, headers) {
612
+ const token = bearerToken(headers);
613
+ if (!token) {
614
+ return {
615
+ ok: false,
616
+ status: 401,
617
+ error: "unauthorized",
618
+ description: "Authorization required",
619
+ challenge: bearerChallenge(o.resource, {
620
+ scope: o.requiredScopes
621
+ })
622
+ };
623
+ }
624
+ let auth;
625
+ try {
626
+ auth = await o.verifyToken(token, {
627
+ resource: o.resource,
628
+ headers
629
+ });
630
+ } catch {
631
+ auth = void 0;
632
+ }
633
+ if (!auth) return deny(o, 401, "invalid_token", "The access token is invalid");
634
+ if (auth.expiresAt !== void 0 && auth.expiresAt * 1e3 <= Date.now()) {
635
+ return deny(o, 401, "invalid_token", "The access token has expired");
636
+ }
637
+ const missing = missingScopes(auth, o.requiredScopes);
638
+ if (missing.length) {
639
+ const scope = [
640
+ .../* @__PURE__ */ new Set([
641
+ ...auth.scopes ?? [],
642
+ ...o.requiredScopes ?? []
643
+ ])
644
+ ];
645
+ return deny(o, 403, "insufficient_scope", `Missing scope: ${missing.join(" ")}`, scope);
646
+ }
647
+ return {
648
+ ok: true,
649
+ auth: {
650
+ ...auth,
651
+ scopes: auth.scopes ?? []
652
+ }
653
+ };
654
+ }
655
+ __name(authenticate, "authenticate");
656
+ function deny(o, status, error, description, scope) {
657
+ return {
658
+ ok: false,
659
+ status,
660
+ error,
661
+ description,
662
+ challenge: bearerChallenge(o.resource, {
663
+ error,
664
+ errorDescription: description,
665
+ scope
666
+ })
667
+ };
668
+ }
669
+ __name(deny, "deny");
670
+ function missingScopes(auth, required) {
671
+ if (!required?.length) return [];
672
+ const granted = new Set(auth?.scopes ?? []);
673
+ return required.filter((s) => !granted.has(s));
674
+ }
675
+ __name(missingScopes, "missingScopes");
676
+
445
677
  // src/core/server.ts
446
678
  var SUPPORTED_PROTOCOL_VERSIONS = [
447
679
  "2025-11-25",
@@ -476,6 +708,7 @@ var McpServer = class {
476
708
  loaders = [];
477
709
  constructor(options) {
478
710
  this.options = options;
711
+ if (options.oauth) validateOAuthOptions(options.oauth);
479
712
  }
480
713
  /** Register a tool. Throws on duplicate names. */
481
714
  addTool(tool) {
@@ -610,6 +843,79 @@ var McpServer = class {
610
843
  return (allowed ?? []).includes(origin);
611
844
  }
612
845
  /**
846
+ * Paths on which adapters serve the OAuth Protected Resource Metadata
847
+ * (empty when `oauth` is not configured). They must be public: no auth middleware.
848
+ */
849
+ get oauthMetadataPaths() {
850
+ return this.options.oauth ? protectedResourceMetadataPaths(this.options.oauth.resource) : [];
851
+ }
852
+ /** The OAuth Protected Resource Metadata document (RFC 9728). */
853
+ async protectedResourceMetadata() {
854
+ const o = this.options.oauth;
855
+ if (!o) throw new Error("mcp-expose: the oauth option is not configured");
856
+ await this.load();
857
+ const scopes = o.scopesSupported ?? [
858
+ .../* @__PURE__ */ new Set([
859
+ ...o.requiredScopes ?? [],
860
+ ...[
861
+ ...this.tools.values()
862
+ ].flatMap((t) => t.scopes ?? [])
863
+ ])
864
+ ];
865
+ return {
866
+ resource: o.resource,
867
+ authorization_servers: o.authorizationServers,
868
+ ...scopes.length ? {
869
+ scopes_supported: scopes
870
+ } : {},
871
+ bearer_methods_supported: [
872
+ "header"
873
+ ],
874
+ ...o.resourceName ? {
875
+ resource_name: o.resourceName
876
+ } : {},
877
+ ...o.resourceDocumentation ? {
878
+ resource_documentation: o.resourceDocumentation
879
+ } : {},
880
+ ...o.metadata
881
+ };
882
+ }
883
+ /** HTTP handler for the metadata paths. Public and CORS-enabled so browser-based clients can read it. */
884
+ async handleMetadataHttp(req) {
885
+ const cors = {
886
+ "access-control-allow-origin": "*",
887
+ "access-control-allow-methods": "GET, OPTIONS"
888
+ };
889
+ const method = req.method.toUpperCase();
890
+ if (!this.options.oauth) return {
891
+ status: 404,
892
+ headers: {}
893
+ };
894
+ if (method === "OPTIONS") return {
895
+ status: 204,
896
+ headers: {
897
+ ...cors,
898
+ "access-control-allow-headers": "*"
899
+ }
900
+ };
901
+ if (method !== "GET" && method !== "HEAD") return {
902
+ status: 405,
903
+ headers: {
904
+ allow: "GET, OPTIONS"
905
+ }
906
+ };
907
+ const body = JSON.stringify(await this.protectedResourceMetadata());
908
+ return {
909
+ status: 200,
910
+ headers: {
911
+ ...cors,
912
+ "content-type": "application/json",
913
+ "cache-control": "public, max-age=3600"
914
+ },
915
+ body: method === "HEAD" ? void 0 : body
916
+ };
917
+ }
918
+ /**
613
919
  * Framework-neutral Streamable HTTP handler. Adapters convert their request
614
920
  * into McpHttpRequest, call this, and write the McpHttpResponse back.
615
921
  */
@@ -624,6 +930,15 @@ var McpServer = class {
624
930
  body: JSON.stringify(fail(null, INVALID_REQUEST, "Origin not allowed"))
625
931
  };
626
932
  }
933
+ const oauth = this.options.oauth;
934
+ if (oauth) {
935
+ const outcome = await authenticate(oauth, req.headers);
936
+ if (!outcome.ok) return authError(outcome);
937
+ ctx = {
938
+ ...ctx,
939
+ auth: outcome.auth
940
+ };
941
+ }
627
942
  if (req.method.toUpperCase() !== "POST") {
628
943
  return {
629
944
  status: 405,
@@ -644,6 +959,27 @@ var McpServer = class {
644
959
  };
645
960
  }
646
961
  }
962
+ if (oauth) {
963
+ await this.load();
964
+ const calls = Array.isArray(body) ? body : [
965
+ body
966
+ ];
967
+ for (const m of calls) {
968
+ if (m?.method !== "tools/call") continue;
969
+ const tool = this.tools.get(m.params?.name);
970
+ const missing = missingScopes(ctx.auth, tool?.scopes);
971
+ if (missing.length) {
972
+ const scope = [
973
+ .../* @__PURE__ */ new Set([
974
+ ...ctx.auth?.scopes ?? [],
975
+ ...oauth.requiredScopes ?? [],
976
+ ...tool?.scopes ?? []
977
+ ])
978
+ ];
979
+ return authError(deny(oauth, 403, "insufficient_scope", `Tool "${tool.name}" needs scope: ${missing.join(" ")}`, scope));
980
+ }
981
+ }
982
+ }
647
983
  const response = await this.handleMessage(body, ctx);
648
984
  if (response === null) return {
649
985
  status: 202,
@@ -656,6 +992,20 @@ var McpServer = class {
656
992
  };
657
993
  }
658
994
  };
995
+ function authError(o) {
996
+ return {
997
+ status: o.status,
998
+ headers: {
999
+ "content-type": "application/json",
1000
+ "www-authenticate": o.challenge
1001
+ },
1002
+ body: JSON.stringify({
1003
+ error: o.error,
1004
+ error_description: o.description
1005
+ })
1006
+ };
1007
+ }
1008
+ __name(authError, "authError");
659
1009
 
660
1010
  // src/express/discover.ts
661
1011
  function collectExpress3(routes, out) {
@@ -777,7 +1127,14 @@ function mountMcp(app, options) {
777
1127
  writeNodeResponse(res, out);
778
1128
  })().catch(next);
779
1129
  }, "handler");
780
- app.all.call(app, path, ...options.middleware ?? [], handler);
1130
+ const all = app.all;
1131
+ all.call(app, path, ...options.middleware ?? [], handler);
1132
+ const metadata = /* @__PURE__ */ __name((req, res, next) => {
1133
+ server.handleMetadataHttp({
1134
+ method: req.method ?? "GET"
1135
+ }).then((out) => writeNodeResponse(res, out)).catch(next);
1136
+ }, "metadata");
1137
+ for (const p of server.oauthMetadataPaths) all.call(app, p, metadata);
781
1138
  return server;
782
1139
  }
783
1140
  __name(mountMcp, "mountMcp");