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
@@ -75,6 +75,38 @@ function loopbackBaseUrl(req) {
75
75
  }
76
76
  __name(loopbackBaseUrl, "loopbackBaseUrl");
77
77
 
78
+ // src/core/result.ts
79
+ function isToolResult(value) {
80
+ const content = value?.content;
81
+ return !!value && typeof value === "object" && Array.isArray(content) && content.length > 0 && content.every((c) => !!c && typeof c === "object" && typeof c.type === "string");
82
+ }
83
+ __name(isToolResult, "isToolResult");
84
+ function toToolResult(value) {
85
+ if (isToolResult(value)) return value;
86
+ if (typeof value === "string") return {
87
+ content: [
88
+ {
89
+ type: "text",
90
+ text: value
91
+ }
92
+ ]
93
+ };
94
+ const text = JSON.stringify(value ?? null);
95
+ const result = {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text
100
+ }
101
+ ]
102
+ };
103
+ if (value && typeof value === "object" && !Array.isArray(value)) {
104
+ result.structuredContent = value;
105
+ }
106
+ return result;
107
+ }
108
+ __name(toToolResult, "toToolResult");
109
+
78
110
  // src/core/schema.ts
79
111
  function isStandard(s) {
80
112
  return typeof s === "object" && s !== null && "~standard" in s;
@@ -295,9 +327,7 @@ function splitArgs(args, mapping) {
295
327
  }
296
328
  __name(splitArgs, "splitArgs");
297
329
  function responseToResult(res, maxChars) {
298
- let text = res.body;
299
- if (text.length > maxChars) text = `${text.slice(0, maxChars)}
300
- \u2026[truncated ${text.length - maxChars} chars]`;
330
+ const text = truncate(res.body, maxChars);
301
331
  const isError = res.status >= 400;
302
332
  const result = {
303
333
  content: [
@@ -322,6 +352,58 @@ ${text}` : text || `HTTP ${res.status}`
322
352
  return result;
323
353
  }
324
354
  __name(responseToResult, "responseToResult");
355
+ function safeDecode(segment) {
356
+ try {
357
+ return decodeURIComponent(segment);
358
+ } catch {
359
+ return segment;
360
+ }
361
+ }
362
+ __name(safeDecode, "safeDecode");
363
+ function truncate(text, maxChars) {
364
+ return text.length > maxChars ? `${text.slice(0, maxChars)}
365
+ \u2026[truncated ${text.length - maxChars} chars]` : text;
366
+ }
367
+ __name(truncate, "truncate");
368
+ function toRouteToolResponse(res) {
369
+ let json;
370
+ if ((res.headers["content-type"] ?? "").includes("json") && res.body) {
371
+ try {
372
+ json = JSON.parse(res.body);
373
+ } catch {
374
+ }
375
+ }
376
+ return {
377
+ ...res,
378
+ json,
379
+ ok: res.status < 400
380
+ };
381
+ }
382
+ __name(toRouteToolResponse, "toRouteToolResponse");
383
+ function transformedResult(value, res, maxChars) {
384
+ const explicit = isToolResult(value);
385
+ const result = toToolResult(value);
386
+ if (!explicit && !res.ok) {
387
+ result.isError = true;
388
+ delete result.structuredContent;
389
+ }
390
+ let truncated = false;
391
+ const content = result.content.map((c) => {
392
+ if (c.type !== "text" || c.text.length <= maxChars) return c;
393
+ truncated = true;
394
+ return {
395
+ ...c,
396
+ text: truncate(c.text, maxChars)
397
+ };
398
+ });
399
+ if (!truncated) return result;
400
+ const { structuredContent: _dropped, ...rest } = result;
401
+ return {
402
+ ...rest,
403
+ content
404
+ };
405
+ }
406
+ __name(transformedResult, "transformedResult");
325
407
  function pickForwardHeaders(ctx, names) {
326
408
  const out = {};
327
409
  for (const name of names) {
@@ -337,6 +419,7 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
337
419
  const { schema, mapping } = buildInputSchema(route, opts);
338
420
  const forward = serverOpts.forwardHeaders ?? DEFAULT_FORWARD_HEADERS;
339
421
  const maxChars = serverOpts.maxResponseChars ?? 1e5;
422
+ const transform = opts.transformResponse ?? serverOpts.transformResponse;
340
423
  return {
341
424
  name,
342
425
  title: opts.title,
@@ -346,6 +429,7 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
346
429
  ...defaultAnnotations(method),
347
430
  ...opts.annotations
348
431
  },
432
+ scopes: opts.scopes,
349
433
  validate: toValidator(opts.input),
350
434
  async handler(args, ctx) {
351
435
  const missing = opts.input ? void 0 : checkRequired(schema, args);
@@ -381,19 +465,167 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
381
465
  ]
382
466
  };
383
467
  }
384
- const res = await dispatch({
468
+ const request = {
385
469
  method,
386
470
  path,
387
471
  query,
388
472
  body,
389
473
  headers
390
- }, ctx);
391
- return responseToResult(res, maxChars);
474
+ };
475
+ const res = await dispatch(request, ctx);
476
+ if (!transform) return responseToResult(res, maxChars);
477
+ const { "content-type": _ct, ...baseHeaders } = headers;
478
+ const hookCtx = {
479
+ tool: name,
480
+ args,
481
+ request,
482
+ toolContext: ctx,
483
+ defaultResult: /* @__PURE__ */ __name(() => responseToResult(res, maxChars), "defaultResult"),
484
+ async callRoute(r) {
485
+ if (!r.path.startsWith("/")) {
486
+ throw new Error(`callRoute: path must start with "/", got ${JSON.stringify(r.path)}`);
487
+ }
488
+ const params = r.params ?? {};
489
+ const missing2 = pathParams(r.path).filter((p) => params[p] === void 0 && !r.path.includes(`:${p}?`));
490
+ if (missing2.length) throw new Error(`callRoute: missing params for ${r.path}: ${missing2.join(", ")}`);
491
+ const subPath = interpolatePath(r.path, params);
492
+ if (subPath.split("/").some((seg) => [
493
+ ".",
494
+ ".."
495
+ ].includes(safeDecode(seg)))) {
496
+ throw new Error(`callRoute: path must not contain "." or ".." segments: ${JSON.stringify(subPath)}`);
497
+ }
498
+ const sub = {
499
+ method: (r.method ?? "GET").toUpperCase(),
500
+ path: subPath,
501
+ query: r.query ?? {},
502
+ body: r.body,
503
+ headers: {
504
+ ...baseHeaders,
505
+ ...r.headers
506
+ }
507
+ };
508
+ if (sub.body !== void 0) sub.headers["content-type"] = "application/json";
509
+ return toRouteToolResponse(await dispatch(sub, ctx));
510
+ }
511
+ };
512
+ const response = toRouteToolResponse(res);
513
+ const value = await transform(response, hookCtx);
514
+ return value === void 0 ? responseToResult(res, maxChars) : transformedResult(value, response, maxChars);
392
515
  }
393
516
  };
394
517
  }
395
518
  __name(createRouteTool, "createRouteTool");
396
519
 
520
+ // src/core/oauth.ts
521
+ var PROTECTED_RESOURCE_WELL_KNOWN = "/.well-known/oauth-protected-resource";
522
+ function protectedResourceMetadataPaths(resource) {
523
+ const pathname = new URL(resource).pathname.replace(/\/+$/, "");
524
+ return pathname ? [
525
+ PROTECTED_RESOURCE_WELL_KNOWN + pathname,
526
+ PROTECTED_RESOURCE_WELL_KNOWN
527
+ ] : [
528
+ PROTECTED_RESOURCE_WELL_KNOWN
529
+ ];
530
+ }
531
+ __name(protectedResourceMetadataPaths, "protectedResourceMetadataPaths");
532
+ function protectedResourceMetadataUrl(resource) {
533
+ return new URL(protectedResourceMetadataPaths(resource)[0], resource).href;
534
+ }
535
+ __name(protectedResourceMetadataUrl, "protectedResourceMetadataUrl");
536
+ function validateOAuthOptions(o) {
537
+ let url;
538
+ try {
539
+ url = new URL(o.resource);
540
+ } catch {
541
+ throw new Error(`mcp-expose: oauth.resource must be an absolute URL, got ${JSON.stringify(o.resource)}`);
542
+ }
543
+ if (url.hash) throw new Error("mcp-expose: oauth.resource must not contain a fragment");
544
+ if (!o.authorizationServers?.length) throw new Error("mcp-expose: oauth.authorizationServers must not be empty");
545
+ if (typeof o.verifyToken !== "function") throw new Error("mcp-expose: oauth.verifyToken is required");
546
+ }
547
+ __name(validateOAuthOptions, "validateOAuthOptions");
548
+ var quote = /* @__PURE__ */ __name((v) => `"${v.replace(/["\\]/g, "\\$&")}"`, "quote");
549
+ function bearerChallenge(resource, params = {}) {
550
+ const parts = [];
551
+ if (params.error) parts.push(`error=${quote(params.error)}`);
552
+ if (params.errorDescription) parts.push(`error_description=${quote(params.errorDescription)}`);
553
+ if (params.scope?.length) parts.push(`scope=${quote(params.scope.join(" "))}`);
554
+ parts.push(`resource_metadata=${quote(protectedResourceMetadataUrl(resource))}`);
555
+ return `Bearer ${parts.join(", ")}`;
556
+ }
557
+ __name(bearerChallenge, "bearerChallenge");
558
+ function bearerToken(headers) {
559
+ const m = /^Bearer[ ]+([^\s,]+)\s*$/i.exec(headers.authorization ?? "");
560
+ return m?.[1];
561
+ }
562
+ __name(bearerToken, "bearerToken");
563
+ async function authenticate(o, headers) {
564
+ const token = bearerToken(headers);
565
+ if (!token) {
566
+ return {
567
+ ok: false,
568
+ status: 401,
569
+ error: "unauthorized",
570
+ description: "Authorization required",
571
+ challenge: bearerChallenge(o.resource, {
572
+ scope: o.requiredScopes
573
+ })
574
+ };
575
+ }
576
+ let auth;
577
+ try {
578
+ auth = await o.verifyToken(token, {
579
+ resource: o.resource,
580
+ headers
581
+ });
582
+ } catch {
583
+ auth = void 0;
584
+ }
585
+ if (!auth) return deny(o, 401, "invalid_token", "The access token is invalid");
586
+ if (auth.expiresAt !== void 0 && auth.expiresAt * 1e3 <= Date.now()) {
587
+ return deny(o, 401, "invalid_token", "The access token has expired");
588
+ }
589
+ const missing = missingScopes(auth, o.requiredScopes);
590
+ if (missing.length) {
591
+ const scope = [
592
+ .../* @__PURE__ */ new Set([
593
+ ...auth.scopes ?? [],
594
+ ...o.requiredScopes ?? []
595
+ ])
596
+ ];
597
+ return deny(o, 403, "insufficient_scope", `Missing scope: ${missing.join(" ")}`, scope);
598
+ }
599
+ return {
600
+ ok: true,
601
+ auth: {
602
+ ...auth,
603
+ scopes: auth.scopes ?? []
604
+ }
605
+ };
606
+ }
607
+ __name(authenticate, "authenticate");
608
+ function deny(o, status, error, description, scope) {
609
+ return {
610
+ ok: false,
611
+ status,
612
+ error,
613
+ description,
614
+ challenge: bearerChallenge(o.resource, {
615
+ error,
616
+ errorDescription: description,
617
+ scope
618
+ })
619
+ };
620
+ }
621
+ __name(deny, "deny");
622
+ function missingScopes(auth, required) {
623
+ if (!required?.length) return [];
624
+ const granted = new Set(auth?.scopes ?? []);
625
+ return required.filter((s) => !granted.has(s));
626
+ }
627
+ __name(missingScopes, "missingScopes");
628
+
397
629
  // src/core/server.ts
398
630
  var SUPPORTED_PROTOCOL_VERSIONS = [
399
631
  "2025-11-25",
@@ -428,6 +660,7 @@ var McpServer = class {
428
660
  loaders = [];
429
661
  constructor(options) {
430
662
  this.options = options;
663
+ if (options.oauth) validateOAuthOptions(options.oauth);
431
664
  }
432
665
  /** Register a tool. Throws on duplicate names. */
433
666
  addTool(tool) {
@@ -562,6 +795,79 @@ var McpServer = class {
562
795
  return (allowed ?? []).includes(origin);
563
796
  }
564
797
  /**
798
+ * Paths on which adapters serve the OAuth Protected Resource Metadata
799
+ * (empty when `oauth` is not configured). They must be public: no auth middleware.
800
+ */
801
+ get oauthMetadataPaths() {
802
+ return this.options.oauth ? protectedResourceMetadataPaths(this.options.oauth.resource) : [];
803
+ }
804
+ /** The OAuth Protected Resource Metadata document (RFC 9728). */
805
+ async protectedResourceMetadata() {
806
+ const o = this.options.oauth;
807
+ if (!o) throw new Error("mcp-expose: the oauth option is not configured");
808
+ await this.load();
809
+ const scopes = o.scopesSupported ?? [
810
+ .../* @__PURE__ */ new Set([
811
+ ...o.requiredScopes ?? [],
812
+ ...[
813
+ ...this.tools.values()
814
+ ].flatMap((t) => t.scopes ?? [])
815
+ ])
816
+ ];
817
+ return {
818
+ resource: o.resource,
819
+ authorization_servers: o.authorizationServers,
820
+ ...scopes.length ? {
821
+ scopes_supported: scopes
822
+ } : {},
823
+ bearer_methods_supported: [
824
+ "header"
825
+ ],
826
+ ...o.resourceName ? {
827
+ resource_name: o.resourceName
828
+ } : {},
829
+ ...o.resourceDocumentation ? {
830
+ resource_documentation: o.resourceDocumentation
831
+ } : {},
832
+ ...o.metadata
833
+ };
834
+ }
835
+ /** HTTP handler for the metadata paths. Public and CORS-enabled so browser-based clients can read it. */
836
+ async handleMetadataHttp(req) {
837
+ const cors = {
838
+ "access-control-allow-origin": "*",
839
+ "access-control-allow-methods": "GET, OPTIONS"
840
+ };
841
+ const method = req.method.toUpperCase();
842
+ if (!this.options.oauth) return {
843
+ status: 404,
844
+ headers: {}
845
+ };
846
+ if (method === "OPTIONS") return {
847
+ status: 204,
848
+ headers: {
849
+ ...cors,
850
+ "access-control-allow-headers": "*"
851
+ }
852
+ };
853
+ if (method !== "GET" && method !== "HEAD") return {
854
+ status: 405,
855
+ headers: {
856
+ allow: "GET, OPTIONS"
857
+ }
858
+ };
859
+ const body = JSON.stringify(await this.protectedResourceMetadata());
860
+ return {
861
+ status: 200,
862
+ headers: {
863
+ ...cors,
864
+ "content-type": "application/json",
865
+ "cache-control": "public, max-age=3600"
866
+ },
867
+ body: method === "HEAD" ? void 0 : body
868
+ };
869
+ }
870
+ /**
565
871
  * Framework-neutral Streamable HTTP handler. Adapters convert their request
566
872
  * into McpHttpRequest, call this, and write the McpHttpResponse back.
567
873
  */
@@ -576,6 +882,15 @@ var McpServer = class {
576
882
  body: JSON.stringify(fail(null, INVALID_REQUEST, "Origin not allowed"))
577
883
  };
578
884
  }
885
+ const oauth = this.options.oauth;
886
+ if (oauth) {
887
+ const outcome = await authenticate(oauth, req.headers);
888
+ if (!outcome.ok) return authError(outcome);
889
+ ctx = {
890
+ ...ctx,
891
+ auth: outcome.auth
892
+ };
893
+ }
579
894
  if (req.method.toUpperCase() !== "POST") {
580
895
  return {
581
896
  status: 405,
@@ -596,6 +911,27 @@ var McpServer = class {
596
911
  };
597
912
  }
598
913
  }
914
+ if (oauth) {
915
+ await this.load();
916
+ const calls = Array.isArray(body) ? body : [
917
+ body
918
+ ];
919
+ for (const m of calls) {
920
+ if (m?.method !== "tools/call") continue;
921
+ const tool = this.tools.get(m.params?.name);
922
+ const missing = missingScopes(ctx.auth, tool?.scopes);
923
+ if (missing.length) {
924
+ const scope = [
925
+ .../* @__PURE__ */ new Set([
926
+ ...ctx.auth?.scopes ?? [],
927
+ ...oauth.requiredScopes ?? [],
928
+ ...tool?.scopes ?? []
929
+ ])
930
+ ];
931
+ return authError(deny(oauth, 403, "insufficient_scope", `Tool "${tool.name}" needs scope: ${missing.join(" ")}`, scope));
932
+ }
933
+ }
934
+ }
599
935
  const response = await this.handleMessage(body, ctx);
600
936
  if (response === null) return {
601
937
  status: 202,
@@ -608,6 +944,20 @@ var McpServer = class {
608
944
  };
609
945
  }
610
946
  };
947
+ function authError(o) {
948
+ return {
949
+ status: o.status,
950
+ headers: {
951
+ "content-type": "application/json",
952
+ "www-authenticate": o.challenge
953
+ },
954
+ body: JSON.stringify({
955
+ error: o.error,
956
+ error_description: o.description
957
+ })
958
+ };
959
+ }
960
+ __name(authError, "authError");
611
961
 
612
962
  // src/adonisjs/index.ts
613
963
  var marked = /* @__PURE__ */ new Map();
@@ -674,6 +1024,16 @@ function mountMcp(router, options) {
674
1024
  });
675
1025
  route.as("mcp_expose.endpoint");
676
1026
  options.configureRoute?.(route);
1027
+ server.oauthMetadataPaths.forEach((p, i) => {
1028
+ router.any(p, async (ctx) => {
1029
+ const out = await server.handleMetadataHttp({
1030
+ method: ctx.request.method()
1031
+ });
1032
+ ctx.response.status(out.status);
1033
+ for (const [k, v] of Object.entries(out.headers)) ctx.response.header(k, v);
1034
+ ctx.response.send(out.body ?? "");
1035
+ }).as(`mcp_expose.oauth_metadata_${i}`);
1036
+ });
677
1037
  return server;
678
1038
  }
679
1039
  __name(mountMcp, "mountMcp");