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
@@ -116,6 +116,38 @@ function loopbackBaseUrl(req) {
116
116
  }
117
117
  __name(loopbackBaseUrl, "loopbackBaseUrl");
118
118
 
119
+ // src/core/result.ts
120
+ function isToolResult(value) {
121
+ const content = value?.content;
122
+ return !!value && typeof value === "object" && Array.isArray(content) && content.length > 0 && content.every((c) => !!c && typeof c === "object" && typeof c.type === "string");
123
+ }
124
+ __name(isToolResult, "isToolResult");
125
+ function toToolResult(value) {
126
+ if (isToolResult(value)) return value;
127
+ if (typeof value === "string") return {
128
+ content: [
129
+ {
130
+ type: "text",
131
+ text: value
132
+ }
133
+ ]
134
+ };
135
+ const text = JSON.stringify(value ?? null);
136
+ const result = {
137
+ content: [
138
+ {
139
+ type: "text",
140
+ text
141
+ }
142
+ ]
143
+ };
144
+ if (value && typeof value === "object" && !Array.isArray(value)) {
145
+ result.structuredContent = value;
146
+ }
147
+ return result;
148
+ }
149
+ __name(toToolResult, "toToolResult");
150
+
119
151
  // src/core/schema.ts
120
152
  function isStandard(s) {
121
153
  return typeof s === "object" && s !== null && "~standard" in s;
@@ -336,9 +368,7 @@ function splitArgs(args, mapping) {
336
368
  }
337
369
  __name(splitArgs, "splitArgs");
338
370
  function responseToResult(res, maxChars) {
339
- let text = res.body;
340
- if (text.length > maxChars) text = `${text.slice(0, maxChars)}
341
- \u2026[truncated ${text.length - maxChars} chars]`;
371
+ const text = truncate(res.body, maxChars);
342
372
  const isError = res.status >= 400;
343
373
  const result = {
344
374
  content: [
@@ -363,6 +393,58 @@ ${text}` : text || `HTTP ${res.status}`
363
393
  return result;
364
394
  }
365
395
  __name(responseToResult, "responseToResult");
396
+ function safeDecode(segment) {
397
+ try {
398
+ return decodeURIComponent(segment);
399
+ } catch {
400
+ return segment;
401
+ }
402
+ }
403
+ __name(safeDecode, "safeDecode");
404
+ function truncate(text, maxChars) {
405
+ return text.length > maxChars ? `${text.slice(0, maxChars)}
406
+ \u2026[truncated ${text.length - maxChars} chars]` : text;
407
+ }
408
+ __name(truncate, "truncate");
409
+ function toRouteToolResponse(res) {
410
+ let json;
411
+ if ((res.headers["content-type"] ?? "").includes("json") && res.body) {
412
+ try {
413
+ json = JSON.parse(res.body);
414
+ } catch {
415
+ }
416
+ }
417
+ return {
418
+ ...res,
419
+ json,
420
+ ok: res.status < 400
421
+ };
422
+ }
423
+ __name(toRouteToolResponse, "toRouteToolResponse");
424
+ function transformedResult(value, res, maxChars) {
425
+ const explicit = isToolResult(value);
426
+ const result = toToolResult(value);
427
+ if (!explicit && !res.ok) {
428
+ result.isError = true;
429
+ delete result.structuredContent;
430
+ }
431
+ let truncated = false;
432
+ const content = result.content.map((c) => {
433
+ if (c.type !== "text" || c.text.length <= maxChars) return c;
434
+ truncated = true;
435
+ return {
436
+ ...c,
437
+ text: truncate(c.text, maxChars)
438
+ };
439
+ });
440
+ if (!truncated) return result;
441
+ const { structuredContent: _dropped, ...rest } = result;
442
+ return {
443
+ ...rest,
444
+ content
445
+ };
446
+ }
447
+ __name(transformedResult, "transformedResult");
366
448
  function pickForwardHeaders(ctx, names) {
367
449
  const out = {};
368
450
  for (const name of names) {
@@ -378,6 +460,7 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
378
460
  const { schema, mapping } = buildInputSchema(route, opts);
379
461
  const forward = serverOpts.forwardHeaders ?? DEFAULT_FORWARD_HEADERS;
380
462
  const maxChars = serverOpts.maxResponseChars ?? 1e5;
463
+ const transform = opts.transformResponse ?? serverOpts.transformResponse;
381
464
  return {
382
465
  name,
383
466
  title: opts.title,
@@ -387,6 +470,7 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
387
470
  ...defaultAnnotations(method),
388
471
  ...opts.annotations
389
472
  },
473
+ scopes: opts.scopes,
390
474
  validate: toValidator(opts.input),
391
475
  async handler(args, ctx) {
392
476
  const missing = opts.input ? void 0 : checkRequired(schema, args);
@@ -422,19 +506,167 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
422
506
  ]
423
507
  };
424
508
  }
425
- const res = await dispatch({
509
+ const request = {
426
510
  method,
427
511
  path,
428
512
  query,
429
513
  body,
430
514
  headers
431
- }, ctx);
432
- return responseToResult(res, maxChars);
515
+ };
516
+ const res = await dispatch(request, ctx);
517
+ if (!transform) return responseToResult(res, maxChars);
518
+ const { "content-type": _ct, ...baseHeaders } = headers;
519
+ const hookCtx = {
520
+ tool: name,
521
+ args,
522
+ request,
523
+ toolContext: ctx,
524
+ defaultResult: /* @__PURE__ */ __name(() => responseToResult(res, maxChars), "defaultResult"),
525
+ async callRoute(r) {
526
+ if (!r.path.startsWith("/")) {
527
+ throw new Error(`callRoute: path must start with "/", got ${JSON.stringify(r.path)}`);
528
+ }
529
+ const params = r.params ?? {};
530
+ const missing2 = pathParams(r.path).filter((p) => params[p] === void 0 && !r.path.includes(`:${p}?`));
531
+ if (missing2.length) throw new Error(`callRoute: missing params for ${r.path}: ${missing2.join(", ")}`);
532
+ const subPath = interpolatePath(r.path, params);
533
+ if (subPath.split("/").some((seg) => [
534
+ ".",
535
+ ".."
536
+ ].includes(safeDecode(seg)))) {
537
+ throw new Error(`callRoute: path must not contain "." or ".." segments: ${JSON.stringify(subPath)}`);
538
+ }
539
+ const sub = {
540
+ method: (r.method ?? "GET").toUpperCase(),
541
+ path: subPath,
542
+ query: r.query ?? {},
543
+ body: r.body,
544
+ headers: {
545
+ ...baseHeaders,
546
+ ...r.headers
547
+ }
548
+ };
549
+ if (sub.body !== void 0) sub.headers["content-type"] = "application/json";
550
+ return toRouteToolResponse(await dispatch(sub, ctx));
551
+ }
552
+ };
553
+ const response = toRouteToolResponse(res);
554
+ const value = await transform(response, hookCtx);
555
+ return value === void 0 ? responseToResult(res, maxChars) : transformedResult(value, response, maxChars);
433
556
  }
434
557
  };
435
558
  }
436
559
  __name(createRouteTool, "createRouteTool");
437
560
 
561
+ // src/core/oauth.ts
562
+ var PROTECTED_RESOURCE_WELL_KNOWN = "/.well-known/oauth-protected-resource";
563
+ function protectedResourceMetadataPaths(resource) {
564
+ const pathname = new URL(resource).pathname.replace(/\/+$/, "");
565
+ return pathname ? [
566
+ PROTECTED_RESOURCE_WELL_KNOWN + pathname,
567
+ PROTECTED_RESOURCE_WELL_KNOWN
568
+ ] : [
569
+ PROTECTED_RESOURCE_WELL_KNOWN
570
+ ];
571
+ }
572
+ __name(protectedResourceMetadataPaths, "protectedResourceMetadataPaths");
573
+ function protectedResourceMetadataUrl(resource) {
574
+ return new URL(protectedResourceMetadataPaths(resource)[0], resource).href;
575
+ }
576
+ __name(protectedResourceMetadataUrl, "protectedResourceMetadataUrl");
577
+ function validateOAuthOptions(o) {
578
+ let url;
579
+ try {
580
+ url = new URL(o.resource);
581
+ } catch {
582
+ throw new Error(`mcp-expose: oauth.resource must be an absolute URL, got ${JSON.stringify(o.resource)}`);
583
+ }
584
+ if (url.hash) throw new Error("mcp-expose: oauth.resource must not contain a fragment");
585
+ if (!o.authorizationServers?.length) throw new Error("mcp-expose: oauth.authorizationServers must not be empty");
586
+ if (typeof o.verifyToken !== "function") throw new Error("mcp-expose: oauth.verifyToken is required");
587
+ }
588
+ __name(validateOAuthOptions, "validateOAuthOptions");
589
+ var quote = /* @__PURE__ */ __name((v) => `"${v.replace(/["\\]/g, "\\$&")}"`, "quote");
590
+ function bearerChallenge(resource, params = {}) {
591
+ const parts = [];
592
+ if (params.error) parts.push(`error=${quote(params.error)}`);
593
+ if (params.errorDescription) parts.push(`error_description=${quote(params.errorDescription)}`);
594
+ if (params.scope?.length) parts.push(`scope=${quote(params.scope.join(" "))}`);
595
+ parts.push(`resource_metadata=${quote(protectedResourceMetadataUrl(resource))}`);
596
+ return `Bearer ${parts.join(", ")}`;
597
+ }
598
+ __name(bearerChallenge, "bearerChallenge");
599
+ function bearerToken(headers) {
600
+ const m = /^Bearer[ ]+([^\s,]+)\s*$/i.exec(headers.authorization ?? "");
601
+ return m?.[1];
602
+ }
603
+ __name(bearerToken, "bearerToken");
604
+ async function authenticate(o, headers) {
605
+ const token = bearerToken(headers);
606
+ if (!token) {
607
+ return {
608
+ ok: false,
609
+ status: 401,
610
+ error: "unauthorized",
611
+ description: "Authorization required",
612
+ challenge: bearerChallenge(o.resource, {
613
+ scope: o.requiredScopes
614
+ })
615
+ };
616
+ }
617
+ let auth;
618
+ try {
619
+ auth = await o.verifyToken(token, {
620
+ resource: o.resource,
621
+ headers
622
+ });
623
+ } catch {
624
+ auth = void 0;
625
+ }
626
+ if (!auth) return deny(o, 401, "invalid_token", "The access token is invalid");
627
+ if (auth.expiresAt !== void 0 && auth.expiresAt * 1e3 <= Date.now()) {
628
+ return deny(o, 401, "invalid_token", "The access token has expired");
629
+ }
630
+ const missing = missingScopes(auth, o.requiredScopes);
631
+ if (missing.length) {
632
+ const scope = [
633
+ .../* @__PURE__ */ new Set([
634
+ ...auth.scopes ?? [],
635
+ ...o.requiredScopes ?? []
636
+ ])
637
+ ];
638
+ return deny(o, 403, "insufficient_scope", `Missing scope: ${missing.join(" ")}`, scope);
639
+ }
640
+ return {
641
+ ok: true,
642
+ auth: {
643
+ ...auth,
644
+ scopes: auth.scopes ?? []
645
+ }
646
+ };
647
+ }
648
+ __name(authenticate, "authenticate");
649
+ function deny(o, status, error, description, scope) {
650
+ return {
651
+ ok: false,
652
+ status,
653
+ error,
654
+ description,
655
+ challenge: bearerChallenge(o.resource, {
656
+ error,
657
+ errorDescription: description,
658
+ scope
659
+ })
660
+ };
661
+ }
662
+ __name(deny, "deny");
663
+ function missingScopes(auth, required) {
664
+ if (!required?.length) return [];
665
+ const granted = new Set(auth?.scopes ?? []);
666
+ return required.filter((s) => !granted.has(s));
667
+ }
668
+ __name(missingScopes, "missingScopes");
669
+
438
670
  // src/core/server.ts
439
671
  var SUPPORTED_PROTOCOL_VERSIONS = [
440
672
  "2025-11-25",
@@ -469,6 +701,7 @@ var McpServer = class {
469
701
  loaders = [];
470
702
  constructor(options) {
471
703
  this.options = options;
704
+ if (options.oauth) validateOAuthOptions(options.oauth);
472
705
  }
473
706
  /** Register a tool. Throws on duplicate names. */
474
707
  addTool(tool) {
@@ -603,6 +836,79 @@ var McpServer = class {
603
836
  return (allowed ?? []).includes(origin);
604
837
  }
605
838
  /**
839
+ * Paths on which adapters serve the OAuth Protected Resource Metadata
840
+ * (empty when `oauth` is not configured). They must be public: no auth middleware.
841
+ */
842
+ get oauthMetadataPaths() {
843
+ return this.options.oauth ? protectedResourceMetadataPaths(this.options.oauth.resource) : [];
844
+ }
845
+ /** The OAuth Protected Resource Metadata document (RFC 9728). */
846
+ async protectedResourceMetadata() {
847
+ const o = this.options.oauth;
848
+ if (!o) throw new Error("mcp-expose: the oauth option is not configured");
849
+ await this.load();
850
+ const scopes = o.scopesSupported ?? [
851
+ .../* @__PURE__ */ new Set([
852
+ ...o.requiredScopes ?? [],
853
+ ...[
854
+ ...this.tools.values()
855
+ ].flatMap((t) => t.scopes ?? [])
856
+ ])
857
+ ];
858
+ return {
859
+ resource: o.resource,
860
+ authorization_servers: o.authorizationServers,
861
+ ...scopes.length ? {
862
+ scopes_supported: scopes
863
+ } : {},
864
+ bearer_methods_supported: [
865
+ "header"
866
+ ],
867
+ ...o.resourceName ? {
868
+ resource_name: o.resourceName
869
+ } : {},
870
+ ...o.resourceDocumentation ? {
871
+ resource_documentation: o.resourceDocumentation
872
+ } : {},
873
+ ...o.metadata
874
+ };
875
+ }
876
+ /** HTTP handler for the metadata paths. Public and CORS-enabled so browser-based clients can read it. */
877
+ async handleMetadataHttp(req) {
878
+ const cors = {
879
+ "access-control-allow-origin": "*",
880
+ "access-control-allow-methods": "GET, OPTIONS"
881
+ };
882
+ const method = req.method.toUpperCase();
883
+ if (!this.options.oauth) return {
884
+ status: 404,
885
+ headers: {}
886
+ };
887
+ if (method === "OPTIONS") return {
888
+ status: 204,
889
+ headers: {
890
+ ...cors,
891
+ "access-control-allow-headers": "*"
892
+ }
893
+ };
894
+ if (method !== "GET" && method !== "HEAD") return {
895
+ status: 405,
896
+ headers: {
897
+ allow: "GET, OPTIONS"
898
+ }
899
+ };
900
+ const body = JSON.stringify(await this.protectedResourceMetadata());
901
+ return {
902
+ status: 200,
903
+ headers: {
904
+ ...cors,
905
+ "content-type": "application/json",
906
+ "cache-control": "public, max-age=3600"
907
+ },
908
+ body: method === "HEAD" ? void 0 : body
909
+ };
910
+ }
911
+ /**
606
912
  * Framework-neutral Streamable HTTP handler. Adapters convert their request
607
913
  * into McpHttpRequest, call this, and write the McpHttpResponse back.
608
914
  */
@@ -617,6 +923,15 @@ var McpServer = class {
617
923
  body: JSON.stringify(fail(null, INVALID_REQUEST, "Origin not allowed"))
618
924
  };
619
925
  }
926
+ const oauth = this.options.oauth;
927
+ if (oauth) {
928
+ const outcome = await authenticate(oauth, req.headers);
929
+ if (!outcome.ok) return authError(outcome);
930
+ ctx = {
931
+ ...ctx,
932
+ auth: outcome.auth
933
+ };
934
+ }
620
935
  if (req.method.toUpperCase() !== "POST") {
621
936
  return {
622
937
  status: 405,
@@ -637,6 +952,27 @@ var McpServer = class {
637
952
  };
638
953
  }
639
954
  }
955
+ if (oauth) {
956
+ await this.load();
957
+ const calls = Array.isArray(body) ? body : [
958
+ body
959
+ ];
960
+ for (const m of calls) {
961
+ if (m?.method !== "tools/call") continue;
962
+ const tool = this.tools.get(m.params?.name);
963
+ const missing = missingScopes(ctx.auth, tool?.scopes);
964
+ if (missing.length) {
965
+ const scope = [
966
+ .../* @__PURE__ */ new Set([
967
+ ...ctx.auth?.scopes ?? [],
968
+ ...oauth.requiredScopes ?? [],
969
+ ...tool?.scopes ?? []
970
+ ])
971
+ ];
972
+ return authError(deny(oauth, 403, "insufficient_scope", `Tool "${tool.name}" needs scope: ${missing.join(" ")}`, scope));
973
+ }
974
+ }
975
+ }
640
976
  const response = await this.handleMessage(body, ctx);
641
977
  if (response === null) return {
642
978
  status: 202,
@@ -649,6 +985,20 @@ var McpServer = class {
649
985
  };
650
986
  }
651
987
  };
988
+ function authError(o) {
989
+ return {
990
+ status: o.status,
991
+ headers: {
992
+ "content-type": "application/json",
993
+ "www-authenticate": o.challenge
994
+ },
995
+ body: JSON.stringify({
996
+ error: o.error,
997
+ error_description: o.description
998
+ })
999
+ };
1000
+ }
1001
+ __name(authError, "authError");
652
1002
 
653
1003
  // src/koa/index.ts
654
1004
  function mcpTool(options = {}) {
@@ -677,7 +1027,17 @@ function createKoaMcp(options) {
677
1027
  }
678
1028
  for (const r of options.routes ?? []) server.addTool(createRouteTool(r, r, dispatch, options));
679
1029
  });
1030
+ const metadataPaths = server.oauthMetadataPaths;
680
1031
  const middleware = /* @__PURE__ */ __name(async (ctx, next) => {
1032
+ if (metadataPaths.includes(ctx.path)) {
1033
+ const out2 = await server.handleMetadataHttp({
1034
+ method: ctx.method
1035
+ });
1036
+ ctx.status = out2.status;
1037
+ ctx.set(out2.headers);
1038
+ if (out2.body !== void 0) ctx.body = out2.body;
1039
+ return;
1040
+ }
681
1041
  if (ctx.path !== path) return next();
682
1042
  const body = ctx.request.body ?? (ctx.method === "POST" ? await readRawBody(ctx.req) : void 0);
683
1043
  const toolCtx = {