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
@@ -93,6 +93,38 @@ function writeNodeResponse(res, out) {
93
93
  }
94
94
  __name(writeNodeResponse, "writeNodeResponse");
95
95
 
96
+ // src/core/result.ts
97
+ function isToolResult(value) {
98
+ const content = value?.content;
99
+ return !!value && typeof value === "object" && Array.isArray(content) && content.length > 0 && content.every((c) => !!c && typeof c === "object" && typeof c.type === "string");
100
+ }
101
+ __name(isToolResult, "isToolResult");
102
+ function toToolResult(value) {
103
+ if (isToolResult(value)) return value;
104
+ if (typeof value === "string") return {
105
+ content: [
106
+ {
107
+ type: "text",
108
+ text: value
109
+ }
110
+ ]
111
+ };
112
+ const text = JSON.stringify(value ?? null);
113
+ const result = {
114
+ content: [
115
+ {
116
+ type: "text",
117
+ text
118
+ }
119
+ ]
120
+ };
121
+ if (value && typeof value === "object" && !Array.isArray(value)) {
122
+ result.structuredContent = value;
123
+ }
124
+ return result;
125
+ }
126
+ __name(toToolResult, "toToolResult");
127
+
96
128
  // src/core/schema.ts
97
129
  function isStandard(s) {
98
130
  return typeof s === "object" && s !== null && "~standard" in s;
@@ -318,9 +350,7 @@ function splitArgs(args, mapping) {
318
350
  }
319
351
  __name(splitArgs, "splitArgs");
320
352
  function responseToResult(res, maxChars) {
321
- let text = res.body;
322
- if (text.length > maxChars) text = `${text.slice(0, maxChars)}
323
- \u2026[truncated ${text.length - maxChars} chars]`;
353
+ const text = truncate(res.body, maxChars);
324
354
  const isError = res.status >= 400;
325
355
  const result = {
326
356
  content: [
@@ -345,6 +375,58 @@ ${text}` : text || `HTTP ${res.status}`
345
375
  return result;
346
376
  }
347
377
  __name(responseToResult, "responseToResult");
378
+ function safeDecode(segment) {
379
+ try {
380
+ return decodeURIComponent(segment);
381
+ } catch {
382
+ return segment;
383
+ }
384
+ }
385
+ __name(safeDecode, "safeDecode");
386
+ function truncate(text, maxChars) {
387
+ return text.length > maxChars ? `${text.slice(0, maxChars)}
388
+ \u2026[truncated ${text.length - maxChars} chars]` : text;
389
+ }
390
+ __name(truncate, "truncate");
391
+ function toRouteToolResponse(res) {
392
+ let json;
393
+ if ((res.headers["content-type"] ?? "").includes("json") && res.body) {
394
+ try {
395
+ json = JSON.parse(res.body);
396
+ } catch {
397
+ }
398
+ }
399
+ return {
400
+ ...res,
401
+ json,
402
+ ok: res.status < 400
403
+ };
404
+ }
405
+ __name(toRouteToolResponse, "toRouteToolResponse");
406
+ function transformedResult(value, res, maxChars) {
407
+ const explicit = isToolResult(value);
408
+ const result = toToolResult(value);
409
+ if (!explicit && !res.ok) {
410
+ result.isError = true;
411
+ delete result.structuredContent;
412
+ }
413
+ let truncated = false;
414
+ const content = result.content.map((c) => {
415
+ if (c.type !== "text" || c.text.length <= maxChars) return c;
416
+ truncated = true;
417
+ return {
418
+ ...c,
419
+ text: truncate(c.text, maxChars)
420
+ };
421
+ });
422
+ if (!truncated) return result;
423
+ const { structuredContent: _dropped, ...rest } = result;
424
+ return {
425
+ ...rest,
426
+ content
427
+ };
428
+ }
429
+ __name(transformedResult, "transformedResult");
348
430
  function pickForwardHeaders(ctx, names) {
349
431
  const out = {};
350
432
  for (const name of names) {
@@ -360,6 +442,7 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
360
442
  const { schema, mapping } = buildInputSchema(route, opts);
361
443
  const forward = serverOpts.forwardHeaders ?? DEFAULT_FORWARD_HEADERS;
362
444
  const maxChars = serverOpts.maxResponseChars ?? 1e5;
445
+ const transform = opts.transformResponse ?? serverOpts.transformResponse;
363
446
  return {
364
447
  name,
365
448
  title: opts.title,
@@ -369,6 +452,7 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
369
452
  ...defaultAnnotations(method),
370
453
  ...opts.annotations
371
454
  },
455
+ scopes: opts.scopes,
372
456
  validate: toValidator(opts.input),
373
457
  async handler(args, ctx) {
374
458
  const missing = opts.input ? void 0 : checkRequired(schema, args);
@@ -404,19 +488,167 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
404
488
  ]
405
489
  };
406
490
  }
407
- const res = await dispatch({
491
+ const request = {
408
492
  method,
409
493
  path,
410
494
  query,
411
495
  body,
412
496
  headers
413
- }, ctx);
414
- return responseToResult(res, maxChars);
497
+ };
498
+ const res = await dispatch(request, ctx);
499
+ if (!transform) return responseToResult(res, maxChars);
500
+ const { "content-type": _ct, ...baseHeaders } = headers;
501
+ const hookCtx = {
502
+ tool: name,
503
+ args,
504
+ request,
505
+ toolContext: ctx,
506
+ defaultResult: /* @__PURE__ */ __name(() => responseToResult(res, maxChars), "defaultResult"),
507
+ async callRoute(r) {
508
+ if (!r.path.startsWith("/")) {
509
+ throw new Error(`callRoute: path must start with "/", got ${JSON.stringify(r.path)}`);
510
+ }
511
+ const params = r.params ?? {};
512
+ const missing2 = pathParams(r.path).filter((p) => params[p] === void 0 && !r.path.includes(`:${p}?`));
513
+ if (missing2.length) throw new Error(`callRoute: missing params for ${r.path}: ${missing2.join(", ")}`);
514
+ const subPath = interpolatePath(r.path, params);
515
+ if (subPath.split("/").some((seg) => [
516
+ ".",
517
+ ".."
518
+ ].includes(safeDecode(seg)))) {
519
+ throw new Error(`callRoute: path must not contain "." or ".." segments: ${JSON.stringify(subPath)}`);
520
+ }
521
+ const sub = {
522
+ method: (r.method ?? "GET").toUpperCase(),
523
+ path: subPath,
524
+ query: r.query ?? {},
525
+ body: r.body,
526
+ headers: {
527
+ ...baseHeaders,
528
+ ...r.headers
529
+ }
530
+ };
531
+ if (sub.body !== void 0) sub.headers["content-type"] = "application/json";
532
+ return toRouteToolResponse(await dispatch(sub, ctx));
533
+ }
534
+ };
535
+ const response = toRouteToolResponse(res);
536
+ const value = await transform(response, hookCtx);
537
+ return value === void 0 ? responseToResult(res, maxChars) : transformedResult(value, response, maxChars);
415
538
  }
416
539
  };
417
540
  }
418
541
  __name(createRouteTool, "createRouteTool");
419
542
 
543
+ // src/core/oauth.ts
544
+ var PROTECTED_RESOURCE_WELL_KNOWN = "/.well-known/oauth-protected-resource";
545
+ function protectedResourceMetadataPaths(resource) {
546
+ const pathname = new URL(resource).pathname.replace(/\/+$/, "");
547
+ return pathname ? [
548
+ PROTECTED_RESOURCE_WELL_KNOWN + pathname,
549
+ PROTECTED_RESOURCE_WELL_KNOWN
550
+ ] : [
551
+ PROTECTED_RESOURCE_WELL_KNOWN
552
+ ];
553
+ }
554
+ __name(protectedResourceMetadataPaths, "protectedResourceMetadataPaths");
555
+ function protectedResourceMetadataUrl(resource) {
556
+ return new URL(protectedResourceMetadataPaths(resource)[0], resource).href;
557
+ }
558
+ __name(protectedResourceMetadataUrl, "protectedResourceMetadataUrl");
559
+ function validateOAuthOptions(o) {
560
+ let url;
561
+ try {
562
+ url = new URL(o.resource);
563
+ } catch {
564
+ throw new Error(`mcp-expose: oauth.resource must be an absolute URL, got ${JSON.stringify(o.resource)}`);
565
+ }
566
+ if (url.hash) throw new Error("mcp-expose: oauth.resource must not contain a fragment");
567
+ if (!o.authorizationServers?.length) throw new Error("mcp-expose: oauth.authorizationServers must not be empty");
568
+ if (typeof o.verifyToken !== "function") throw new Error("mcp-expose: oauth.verifyToken is required");
569
+ }
570
+ __name(validateOAuthOptions, "validateOAuthOptions");
571
+ var quote = /* @__PURE__ */ __name((v) => `"${v.replace(/["\\]/g, "\\$&")}"`, "quote");
572
+ function bearerChallenge(resource, params = {}) {
573
+ const parts = [];
574
+ if (params.error) parts.push(`error=${quote(params.error)}`);
575
+ if (params.errorDescription) parts.push(`error_description=${quote(params.errorDescription)}`);
576
+ if (params.scope?.length) parts.push(`scope=${quote(params.scope.join(" "))}`);
577
+ parts.push(`resource_metadata=${quote(protectedResourceMetadataUrl(resource))}`);
578
+ return `Bearer ${parts.join(", ")}`;
579
+ }
580
+ __name(bearerChallenge, "bearerChallenge");
581
+ function bearerToken(headers) {
582
+ const m = /^Bearer[ ]+([^\s,]+)\s*$/i.exec(headers.authorization ?? "");
583
+ return m?.[1];
584
+ }
585
+ __name(bearerToken, "bearerToken");
586
+ async function authenticate(o, headers) {
587
+ const token = bearerToken(headers);
588
+ if (!token) {
589
+ return {
590
+ ok: false,
591
+ status: 401,
592
+ error: "unauthorized",
593
+ description: "Authorization required",
594
+ challenge: bearerChallenge(o.resource, {
595
+ scope: o.requiredScopes
596
+ })
597
+ };
598
+ }
599
+ let auth;
600
+ try {
601
+ auth = await o.verifyToken(token, {
602
+ resource: o.resource,
603
+ headers
604
+ });
605
+ } catch {
606
+ auth = void 0;
607
+ }
608
+ if (!auth) return deny(o, 401, "invalid_token", "The access token is invalid");
609
+ if (auth.expiresAt !== void 0 && auth.expiresAt * 1e3 <= Date.now()) {
610
+ return deny(o, 401, "invalid_token", "The access token has expired");
611
+ }
612
+ const missing = missingScopes(auth, o.requiredScopes);
613
+ if (missing.length) {
614
+ const scope = [
615
+ .../* @__PURE__ */ new Set([
616
+ ...auth.scopes ?? [],
617
+ ...o.requiredScopes ?? []
618
+ ])
619
+ ];
620
+ return deny(o, 403, "insufficient_scope", `Missing scope: ${missing.join(" ")}`, scope);
621
+ }
622
+ return {
623
+ ok: true,
624
+ auth: {
625
+ ...auth,
626
+ scopes: auth.scopes ?? []
627
+ }
628
+ };
629
+ }
630
+ __name(authenticate, "authenticate");
631
+ function deny(o, status, error, description, scope) {
632
+ return {
633
+ ok: false,
634
+ status,
635
+ error,
636
+ description,
637
+ challenge: bearerChallenge(o.resource, {
638
+ error,
639
+ errorDescription: description,
640
+ scope
641
+ })
642
+ };
643
+ }
644
+ __name(deny, "deny");
645
+ function missingScopes(auth, required) {
646
+ if (!required?.length) return [];
647
+ const granted = new Set(auth?.scopes ?? []);
648
+ return required.filter((s) => !granted.has(s));
649
+ }
650
+ __name(missingScopes, "missingScopes");
651
+
420
652
  // src/core/server.ts
421
653
  var SUPPORTED_PROTOCOL_VERSIONS = [
422
654
  "2025-11-25",
@@ -451,6 +683,7 @@ var McpServer = class {
451
683
  loaders = [];
452
684
  constructor(options) {
453
685
  this.options = options;
686
+ if (options.oauth) validateOAuthOptions(options.oauth);
454
687
  }
455
688
  /** Register a tool. Throws on duplicate names. */
456
689
  addTool(tool) {
@@ -585,6 +818,79 @@ var McpServer = class {
585
818
  return (allowed ?? []).includes(origin);
586
819
  }
587
820
  /**
821
+ * Paths on which adapters serve the OAuth Protected Resource Metadata
822
+ * (empty when `oauth` is not configured). They must be public: no auth middleware.
823
+ */
824
+ get oauthMetadataPaths() {
825
+ return this.options.oauth ? protectedResourceMetadataPaths(this.options.oauth.resource) : [];
826
+ }
827
+ /** The OAuth Protected Resource Metadata document (RFC 9728). */
828
+ async protectedResourceMetadata() {
829
+ const o = this.options.oauth;
830
+ if (!o) throw new Error("mcp-expose: the oauth option is not configured");
831
+ await this.load();
832
+ const scopes = o.scopesSupported ?? [
833
+ .../* @__PURE__ */ new Set([
834
+ ...o.requiredScopes ?? [],
835
+ ...[
836
+ ...this.tools.values()
837
+ ].flatMap((t) => t.scopes ?? [])
838
+ ])
839
+ ];
840
+ return {
841
+ resource: o.resource,
842
+ authorization_servers: o.authorizationServers,
843
+ ...scopes.length ? {
844
+ scopes_supported: scopes
845
+ } : {},
846
+ bearer_methods_supported: [
847
+ "header"
848
+ ],
849
+ ...o.resourceName ? {
850
+ resource_name: o.resourceName
851
+ } : {},
852
+ ...o.resourceDocumentation ? {
853
+ resource_documentation: o.resourceDocumentation
854
+ } : {},
855
+ ...o.metadata
856
+ };
857
+ }
858
+ /** HTTP handler for the metadata paths. Public and CORS-enabled so browser-based clients can read it. */
859
+ async handleMetadataHttp(req) {
860
+ const cors = {
861
+ "access-control-allow-origin": "*",
862
+ "access-control-allow-methods": "GET, OPTIONS"
863
+ };
864
+ const method = req.method.toUpperCase();
865
+ if (!this.options.oauth) return {
866
+ status: 404,
867
+ headers: {}
868
+ };
869
+ if (method === "OPTIONS") return {
870
+ status: 204,
871
+ headers: {
872
+ ...cors,
873
+ "access-control-allow-headers": "*"
874
+ }
875
+ };
876
+ if (method !== "GET" && method !== "HEAD") return {
877
+ status: 405,
878
+ headers: {
879
+ allow: "GET, OPTIONS"
880
+ }
881
+ };
882
+ const body = JSON.stringify(await this.protectedResourceMetadata());
883
+ return {
884
+ status: 200,
885
+ headers: {
886
+ ...cors,
887
+ "content-type": "application/json",
888
+ "cache-control": "public, max-age=3600"
889
+ },
890
+ body: method === "HEAD" ? void 0 : body
891
+ };
892
+ }
893
+ /**
588
894
  * Framework-neutral Streamable HTTP handler. Adapters convert their request
589
895
  * into McpHttpRequest, call this, and write the McpHttpResponse back.
590
896
  */
@@ -599,6 +905,15 @@ var McpServer = class {
599
905
  body: JSON.stringify(fail(null, INVALID_REQUEST, "Origin not allowed"))
600
906
  };
601
907
  }
908
+ const oauth = this.options.oauth;
909
+ if (oauth) {
910
+ const outcome = await authenticate(oauth, req.headers);
911
+ if (!outcome.ok) return authError(outcome);
912
+ ctx = {
913
+ ...ctx,
914
+ auth: outcome.auth
915
+ };
916
+ }
602
917
  if (req.method.toUpperCase() !== "POST") {
603
918
  return {
604
919
  status: 405,
@@ -619,6 +934,27 @@ var McpServer = class {
619
934
  };
620
935
  }
621
936
  }
937
+ if (oauth) {
938
+ await this.load();
939
+ const calls = Array.isArray(body) ? body : [
940
+ body
941
+ ];
942
+ for (const m of calls) {
943
+ if (m?.method !== "tools/call") continue;
944
+ const tool = this.tools.get(m.params?.name);
945
+ const missing = missingScopes(ctx.auth, tool?.scopes);
946
+ if (missing.length) {
947
+ const scope = [
948
+ .../* @__PURE__ */ new Set([
949
+ ...ctx.auth?.scopes ?? [],
950
+ ...oauth.requiredScopes ?? [],
951
+ ...tool?.scopes ?? []
952
+ ])
953
+ ];
954
+ return authError(deny(oauth, 403, "insufficient_scope", `Tool "${tool.name}" needs scope: ${missing.join(" ")}`, scope));
955
+ }
956
+ }
957
+ }
622
958
  const response = await this.handleMessage(body, ctx);
623
959
  if (response === null) return {
624
960
  status: 202,
@@ -631,6 +967,20 @@ var McpServer = class {
631
967
  };
632
968
  }
633
969
  };
970
+ function authError(o) {
971
+ return {
972
+ status: o.status,
973
+ headers: {
974
+ "content-type": "application/json",
975
+ "www-authenticate": o.challenge
976
+ },
977
+ body: JSON.stringify({
978
+ error: o.error,
979
+ error_description: o.description
980
+ })
981
+ };
982
+ }
983
+ __name(authError, "authError");
634
984
 
635
985
  // src/express/discover.ts
636
986
  function collectExpress3(routes, out) {
@@ -752,7 +1102,14 @@ function mountMcp(app, options) {
752
1102
  writeNodeResponse(res, out);
753
1103
  })().catch(next);
754
1104
  }, "handler");
755
- app.all.call(app, path, ...options.middleware ?? [], handler);
1105
+ const all = app.all;
1106
+ all.call(app, path, ...options.middleware ?? [], handler);
1107
+ const metadata = /* @__PURE__ */ __name((req, res, next) => {
1108
+ server.handleMetadataHttp({
1109
+ method: req.method ?? "GET"
1110
+ }).then((out) => writeNodeResponse(res, out)).catch(next);
1111
+ }, "metadata");
1112
+ for (const p of server.oauthMetadataPaths) all.call(app, p, metadata);
756
1113
  return server;
757
1114
  }
758
1115
  __name(mountMcp, "mountMcp");