@solvapay/mcp-core 0.2.3 → 0.2.4

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.
package/dist/index.cjs CHANGED
@@ -53,6 +53,7 @@ __export(index_exports, {
53
53
  createBuildBootstrapPayload: () => createBuildBootstrapPayload,
54
54
  decodeJwtPayload: () => decodeJwtPayload,
55
55
  defaultGetCustomerRef: () => defaultGetCustomerRef,
56
+ defaultIsChatGptRequest: () => defaultIsChatGptRequest,
56
57
  deriveIcons: () => deriveIcons,
57
58
  enrichPurchase: () => enrichPurchase,
58
59
  extractBearerToken: () => extractBearerToken,
@@ -503,9 +504,24 @@ function mergeCsp(overrides, apiBaseUrl) {
503
504
  }
504
505
 
505
506
  // src/hideToolsByAudience.ts
506
- function applyHideToolsByAudience(server, audiences) {
507
+ var CHATGPT_CLIENT_RE = /openai-mcp/i;
508
+ function readHeader(headers, name) {
509
+ if (!headers) return void 0;
510
+ const raw = headers[name] ?? headers[name.toLowerCase()];
511
+ if (Array.isArray(raw)) return raw[0];
512
+ return typeof raw === "string" ? raw : void 0;
513
+ }
514
+ function defaultIsChatGptRequest(ctx) {
515
+ const ua = readHeader(ctx.extra?.requestInfo?.headers, "user-agent");
516
+ if (ua && CHATGPT_CLIENT_RE.test(ua)) return true;
517
+ const clientVersion = ctx.server?.server?.getClientVersion?.();
518
+ const clientName = typeof clientVersion?.name === "string" ? clientVersion.name : void 0;
519
+ return clientName !== void 0 && CHATGPT_CLIENT_RE.test(clientName);
520
+ }
521
+ function applyHideToolsByAudience(server, audiences, options = {}) {
507
522
  if (!audiences || audiences.length === 0) return;
508
523
  const hidden = new Set(audiences);
524
+ const bypassWhen = options.bypassWhen ?? defaultIsChatGptRequest;
509
525
  const inner = server.server;
510
526
  if (!inner || typeof inner !== "object" || !(inner._requestHandlers instanceof Map)) {
511
527
  return;
@@ -513,8 +529,26 @@ function applyHideToolsByAudience(server, audiences) {
513
529
  const handlers = inner._requestHandlers;
514
530
  const original = handlers.get("tools/list");
515
531
  if (!original) return;
532
+ const warned = /* @__PURE__ */ new Set();
516
533
  handlers.set("tools/list", async (req, extra) => {
517
534
  const res = await original(req, extra);
535
+ if (bypassWhen({
536
+ server,
537
+ extra
538
+ })) {
539
+ const ua = readHeader(
540
+ extra?.requestInfo?.headers,
541
+ "user-agent"
542
+ );
543
+ const context = ua ? `ua=${ua}` : "no user-agent";
544
+ if (!warned.has(context)) {
545
+ warned.add(context);
546
+ console.warn(
547
+ `[solvapay/mcp] hideToolsByAudience filter bypassed (${context}); returning full tools/list catalog.`
548
+ );
549
+ }
550
+ return res;
551
+ }
518
552
  const tools = Array.isArray(res?.tools) ? res.tools : [];
519
553
  return {
520
554
  ...res,
@@ -766,7 +800,10 @@ function buildSolvaPayDescriptors(options) {
766
800
  handler: async (args, extra) => trace(name, args, extra, async () => {
767
801
  const mode = parseMode(args.mode);
768
802
  const data = await buildBootstrapPayload(view, extra);
769
- return narratedToolResult(name, data, mode, toolMeta);
803
+ return narratedToolResult(name, data, mode, {
804
+ ...toolMeta,
805
+ "openai/widgetSessionId": crypto.randomUUID()
806
+ });
770
807
  })
771
808
  });
772
809
  };
@@ -967,7 +1004,7 @@ function buildSolvaPayDescriptors(options) {
967
1004
  MCP_TOOL_NAMES.activatePlan,
968
1005
  await buildBootstrapPayload("checkout", extra),
969
1006
  mode,
970
- toolMeta
1007
+ { ...toolMeta, "openai/widgetSessionId": crypto.randomUUID() }
971
1008
  );
972
1009
  }
973
1010
  const auth = requireCustomerRef(extra);
@@ -1377,6 +1414,7 @@ function buildAuthInfoFromBearer(authorization, options = {}) {
1377
1414
  createBuildBootstrapPayload,
1378
1415
  decodeJwtPayload,
1379
1416
  defaultGetCustomerRef,
1417
+ defaultIsChatGptRequest,
1380
1418
  deriveIcons,
1381
1419
  enrichPurchase,
1382
1420
  extractBearerToken,
package/dist/index.d.cts CHANGED
@@ -734,6 +734,12 @@ declare function parseMode(raw: unknown): SolvaPayToolMode;
734
734
  *
735
735
  * The narrator is picked by the `tool` name; unknown tools fall back
736
736
  * to the JSON dump that `toolResult` produces today.
737
+ *
738
+ * Meta keys other than `ui` (notably `openai/widgetSessionId`, the
739
+ * ChatGPT MCP routing-bug workaround stamped by intent tools — see
740
+ * the descriptors.ts top-of-file comment) are preserved across all
741
+ * three modes. Don't strip additional `_meta` keys here without
742
+ * checking `descriptors.ts` first.
737
743
  */
738
744
  declare function narratedToolResult(tool: IntentTool | string, data: BootstrapPayload, mode?: SolvaPayToolMode, baseMeta?: Record<string, unknown> | undefined): SolvaPayCallToolResult;
739
745
  /**
@@ -861,7 +867,7 @@ declare const SOLVAPAY_DEFAULT_CSP: Required<SolvaPayMcpCsp>;
861
867
  declare function mergeCsp(overrides: SolvaPayMcpCsp | undefined, apiBaseUrl?: string): Required<SolvaPayMcpCsp>;
862
868
 
863
869
  /**
864
- * `applyHideToolsByAudience(server, audiences)` — wraps the
870
+ * `applyHideToolsByAudience(server, audiences, options?)` — wraps the
865
871
  * `tools/list` request handler on an `@modelcontextprotocol/sdk`
866
872
  * `McpServer` so tool descriptors whose `_meta.audience` matches one
867
873
  * of the supplied values are filtered out of the response.
@@ -869,21 +875,50 @@ declare function mergeCsp(overrides: SolvaPayMcpCsp | undefined, apiBaseUrl?: st
869
875
  * The tools stay `enabled: true` on the server, so `tools/call` still
870
876
  * reaches their handlers — this helper only affects the `tools/list`
871
877
  * response shape. Use `['ui']` when deploying to a text-host MCP
872
- * client (Claude Desktop, MCPJam, ChatGPT connectors) that won't
878
+ * client (Claude Desktop, MCPJam, ChatGPT connector) that won't
873
879
  * embed the SolvaPay iframe surface, while still allowing the iframe
874
880
  * to invoke the hidden transport tools (`create_payment_intent` etc.)
875
881
  * for server-side work.
876
882
  *
877
- * Internal-map reach-in rationale: `Protocol.setRequestHandler`
878
- * contains an `assertCanSetRequestHandler` guard that fails when the
879
- * SDK has already registered a handler for the method during
880
- * registration i.e. `tools/list` after any tool registration. The
881
- * guard only fires through the public wrapper; the underlying
882
- * `_requestHandlers` map accepts a replacement silently. Touching the
883
- * map directly therefore sidesteps the guard without going through
884
- * public API surface that could change in a minor SDK bump. The one
885
- * piece of SDK-internal knowledge we live with until the SDK ships a
886
- * first-class "replace handler" affordance.
883
+ * # ChatGPT auto-bypass
884
+ *
885
+ * ChatGPT's Custom Connector gateway re-validates iframe-initiated
886
+ * `tools/call` against the cached `tools/list` catalog. A tool hidden
887
+ * from `tools/list` becomes uncallable from the embedded iframe and
888
+ * surfaces in the UI as `MCP error -32000: MCP Resource not found`.
889
+ *
890
+ * To keep the cleaner LLM-facing catalog on every other host while
891
+ * the iframe still works on ChatGPT, the default behaviour
892
+ * automatically returns the **full** unfiltered catalog when the
893
+ * incoming `tools/list` request originates from ChatGPT — detected by
894
+ * matching `request.headers['user-agent']` and the post-`initialize`
895
+ * `server.getClientVersion().name` against `/openai-mcp/i`. The first
896
+ * triggers on the discovery `tools/list` ChatGPT issues before
897
+ * `initialize` (so `getClientVersion()` is empty); the second is a
898
+ * defence-in-depth fallback for any future relay that strips the
899
+ * client UA.
900
+ *
901
+ * The User-Agent shape was confirmed live against ChatGPT's MCP
902
+ * runtime (`openai-mcp/1.0.0 (ChatGPT)` as of 2026-05). The pattern
903
+ * is intentionally broad so a UA bump to `openai-mcp/2.x` keeps
904
+ * working without code changes.
905
+ *
906
+ * Override the detection by passing `bypassWhen` — useful when a
907
+ * future iframe-capable host needs the same treatment, or when
908
+ * ChatGPT-served deployments want the LLM-narrow catalog regardless
909
+ * (`bypassWhen: () => false`).
910
+ *
911
+ * # Internal-map reach-in rationale
912
+ *
913
+ * `Protocol.setRequestHandler` contains an `assertCanSetRequestHandler`
914
+ * guard that fails when the SDK has already registered a handler for
915
+ * the method during registration — i.e. `tools/list` after any tool
916
+ * registration. The guard only fires through the public wrapper; the
917
+ * underlying `_requestHandlers` map accepts a replacement silently.
918
+ * Touching the map directly therefore sidesteps the guard without
919
+ * going through public API surface that could change in a minor SDK
920
+ * bump. The one piece of SDK-internal knowledge we live with until
921
+ * the SDK ships a first-class "replace handler" affordance.
887
922
  *
888
923
  * No-op when `audiences` is empty or falsy.
889
924
  *
@@ -892,7 +927,58 @@ declare function mergeCsp(overrides: SolvaPayMcpCsp | undefined, apiBaseUrl?: st
892
927
  * `@solvapay/mcp/fetch` (unified `createSolvaPayMcpFetch`) can apply
893
928
  * the same filter without each re-implementing the reach-in.
894
929
  */
895
- declare function applyHideToolsByAudience(server: unknown, audiences: readonly string[] | undefined): void;
930
+ /**
931
+ * Headers shape exposed by the MCP SDK's `RequestHandlerExtra.requestInfo`.
932
+ * Mirrors `IsomorphicHeaders` from the SDK without taking a type dep.
933
+ */
934
+ type IsomorphicHeaders = Record<string, string | string[] | undefined>;
935
+ /**
936
+ * Subset of `RequestHandlerExtra` (from `@modelcontextprotocol/sdk`)
937
+ * that the bypass predicate inspects. Typed loosely so we don't
938
+ * couple to the SDK's exact shape.
939
+ */
940
+ interface ApplyHideToolsByAudienceExtra {
941
+ requestInfo?: {
942
+ headers?: IsomorphicHeaders;
943
+ } | undefined;
944
+ [key: string]: unknown;
945
+ }
946
+ interface ApplyHideToolsByAudienceContext {
947
+ /** The MCP server instance the filter is being applied to. */
948
+ server: unknown;
949
+ /**
950
+ * The `RequestHandlerExtra` the SDK passed to the wrapped
951
+ * `tools/list` handler. May be undefined for non-HTTP transports
952
+ * (e.g. stdio) where there's no request information.
953
+ */
954
+ extra?: ApplyHideToolsByAudienceExtra;
955
+ }
956
+ type HideToolsByAudienceBypass = (ctx: ApplyHideToolsByAudienceContext) => boolean;
957
+ interface ApplyHideToolsByAudienceOptions {
958
+ /**
959
+ * When this predicate returns `true` for an incoming `tools/list`
960
+ * request, the audience filter is skipped and the full catalog is
961
+ * returned. Defaults to `defaultIsChatGptRequest` — see the file
962
+ * header for the rationale.
963
+ *
964
+ * Pass an integrator-supplied predicate to extend the bypass to
965
+ * another host (e.g. a future iframe-capable client), or `() =>
966
+ * false` to apply the filter unconditionally.
967
+ */
968
+ bypassWhen?: HideToolsByAudienceBypass;
969
+ }
970
+ /**
971
+ * Default `bypassWhen` — returns true when the incoming request looks
972
+ * like it's coming from ChatGPT's MCP runtime. Two signals:
973
+ *
974
+ * 1. HTTP `User-Agent` header (works on the pre-`initialize`
975
+ * discovery `tools/list`, so we have a signal even before the
976
+ * SDK's `getClientVersion()` is populated).
977
+ * 2. `server.getClientVersion()?.name` (covers any host that relays
978
+ * ChatGPT requests without forwarding the upstream UA).
979
+ */
980
+ declare function defaultIsChatGptRequest(ctx: ApplyHideToolsByAudienceContext): boolean;
981
+ declare function applyHideToolsByAudience(server: unknown, audiences: readonly string[] | undefined, options?: ApplyHideToolsByAudienceOptions): void;
896
982
 
897
983
  /**
898
984
  * `buildSolvaPayDescriptors(options)` — framework-neutral tool surface
@@ -905,6 +991,28 @@ declare function applyHideToolsByAudience(server: unknown, audiences: readonly s
905
991
  * mechanical difference: instead of calling `registerAppTool(server, ...)`,
906
992
  * we push `{ name, handler, ... }` onto a `tools[]` array the adapter
907
993
  * iterates.
994
+ *
995
+ * ----
996
+ *
997
+ * `_meta["openai/widgetSessionId"]` workaround. Every intent-tool
998
+ * response stamps a freshly-minted UUID on `_meta["openai/widgetSessionId"]`.
999
+ * This is a low-risk forward-looking workaround for the ChatGPT MCP
1000
+ * connector's stale `link_<id>` routing bug, where the host returns
1001
+ * `-32000 MCP Resource not found` on the second `tools/call` of a
1002
+ * session even though the call never reaches the server. A fresh UUID
1003
+ * per invocation gives the host a routing key that changes every call,
1004
+ * which the OpenAI Apps SDK community thread reports unsticks the
1005
+ * failure mode.
1006
+ *
1007
+ * Sources:
1008
+ * - https://community.openai.com/t/connector-tool-calls-generating-fresh-mcp-session-each-invocation/1364975
1009
+ * - https://github.com/openai/openai-apps-sdk-examples/issues/165
1010
+ * - https://developers.openai.com/apps-sdk/reference/ (`_meta` payload)
1011
+ * - openai/openai-apps-sdk-examples shopping_cart_python uses the
1012
+ * same `meta["openai/widgetSessionId"]` shape.
1013
+ *
1014
+ * Removable once the upstream bug ships a fix; safe on any host that
1015
+ * doesn't consume the key.
908
1016
  */
909
1017
 
910
1018
  /**
@@ -1196,4 +1304,4 @@ interface BuildAuthInfoFromBearerOptions extends McpBearerCustomerRefOptions {
1196
1304
  }
1197
1305
  declare function buildAuthInfoFromBearer(authorization?: string | null, options?: BuildAuthInfoFromBearerOptions): McpToolExtra['authInfo'] | null;
1198
1306
 
1199
- export { type BootstrapCustomer, type BootstrapMerchant, type BootstrapPayload, type BootstrapPlan, type BootstrapProduct, type BuildAuthInfoFromBearerOptions, type BuildBootstrapPayloadFn, type BuildPayableHandlerContext, type BuildSolvaPayDescriptorsOptions, type BuildSolvaPayRequestOptions, type ContentBlock, type CreateBuildBootstrapPayloadOptions, type CustomerSnapshot, DEFAULT_OAUTH_PATHS, type IntentTool, MCP_TOOL_NAMES, type McpAdapterOptions, McpBearerAuthError, type McpBearerCustomerRefOptions, type McpToolExtra, type McpToolName, NARRATORS, type NarratorOutput, type NudgeSpec, type OAuthAuthorizationServerOptions, type OAuthBridgePaths, OPEN_TOOL_FOR_VIEW, type PayableHandler, type PaywallToolResult, type PaywallToolResultContext, type ResponseContext, type ResponseOptions, type ResponseResult, SOLVAPAY_DEFAULT_CSP, SOLVAPAY_MCP_VIEW_KINDS, SOLVAPAY_OVERVIEW_MARKDOWN, SOLVAPAY_OVERVIEW_MIME_TYPE, SOLVAPAY_OVERVIEW_URI, type SolvaPayCallToolResult, type SolvaPayDescriptorBundle, type SolvaPayDocsResourceDescriptor, type SolvaPayMcpCsp, type SolvaPayMcpViewKind, type SolvaPayMerchantBranding, type SolvaPayPromptDescriptor, type SolvaPayPromptResult, type SolvaPayResourceDescriptor, type SolvaPayToolAnnotations, type SolvaPayToolDescriptor, type SolvaPayToolIcon, type SolvaPayToolMode, TOOL_FOR_VIEW, VIEW_FOR_OPEN_TOOL, VIEW_FOR_TOOL, applyHideToolsByAudience, balanceSummary, buildAuthInfoFromBearer, buildPayableHandler, buildSolvaPayDescriptors, buildSolvaPayPrompts, buildSolvaPayRequest, createBuildBootstrapPayload, decodeJwtPayload, defaultGetCustomerRef, deriveIcons, enrichPurchase, extractBearerToken, getCustomerRefFromBearerAuthHeader, getCustomerRefFromJwtPayload, getOAuthAuthorizationServerResponse, getOAuthProtectedResourceResponse, mergeCsp, narrateActivatePlan, narrateManageAccount, narrateTopup, narrateUpgrade, narratedToolResult, parseMode, paywallToolResult, previewJson, resolveOAuthPaths, toolErrorResult, toolResult, uiPlaceholder, withoutTrailingSlash };
1307
+ export { type ApplyHideToolsByAudienceContext, type ApplyHideToolsByAudienceExtra, type ApplyHideToolsByAudienceOptions, type BootstrapCustomer, type BootstrapMerchant, type BootstrapPayload, type BootstrapPlan, type BootstrapProduct, type BuildAuthInfoFromBearerOptions, type BuildBootstrapPayloadFn, type BuildPayableHandlerContext, type BuildSolvaPayDescriptorsOptions, type BuildSolvaPayRequestOptions, type ContentBlock, type CreateBuildBootstrapPayloadOptions, type CustomerSnapshot, DEFAULT_OAUTH_PATHS, type HideToolsByAudienceBypass, type IntentTool, MCP_TOOL_NAMES, type McpAdapterOptions, McpBearerAuthError, type McpBearerCustomerRefOptions, type McpToolExtra, type McpToolName, NARRATORS, type NarratorOutput, type NudgeSpec, type OAuthAuthorizationServerOptions, type OAuthBridgePaths, OPEN_TOOL_FOR_VIEW, type PayableHandler, type PaywallToolResult, type PaywallToolResultContext, type ResponseContext, type ResponseOptions, type ResponseResult, SOLVAPAY_DEFAULT_CSP, SOLVAPAY_MCP_VIEW_KINDS, SOLVAPAY_OVERVIEW_MARKDOWN, SOLVAPAY_OVERVIEW_MIME_TYPE, SOLVAPAY_OVERVIEW_URI, type SolvaPayCallToolResult, type SolvaPayDescriptorBundle, type SolvaPayDocsResourceDescriptor, type SolvaPayMcpCsp, type SolvaPayMcpViewKind, type SolvaPayMerchantBranding, type SolvaPayPromptDescriptor, type SolvaPayPromptResult, type SolvaPayResourceDescriptor, type SolvaPayToolAnnotations, type SolvaPayToolDescriptor, type SolvaPayToolIcon, type SolvaPayToolMode, TOOL_FOR_VIEW, VIEW_FOR_OPEN_TOOL, VIEW_FOR_TOOL, applyHideToolsByAudience, balanceSummary, buildAuthInfoFromBearer, buildPayableHandler, buildSolvaPayDescriptors, buildSolvaPayPrompts, buildSolvaPayRequest, createBuildBootstrapPayload, decodeJwtPayload, defaultGetCustomerRef, defaultIsChatGptRequest, deriveIcons, enrichPurchase, extractBearerToken, getCustomerRefFromBearerAuthHeader, getCustomerRefFromJwtPayload, getOAuthAuthorizationServerResponse, getOAuthProtectedResourceResponse, mergeCsp, narrateActivatePlan, narrateManageAccount, narrateTopup, narrateUpgrade, narratedToolResult, parseMode, paywallToolResult, previewJson, resolveOAuthPaths, toolErrorResult, toolResult, uiPlaceholder, withoutTrailingSlash };
package/dist/index.d.ts CHANGED
@@ -734,6 +734,12 @@ declare function parseMode(raw: unknown): SolvaPayToolMode;
734
734
  *
735
735
  * The narrator is picked by the `tool` name; unknown tools fall back
736
736
  * to the JSON dump that `toolResult` produces today.
737
+ *
738
+ * Meta keys other than `ui` (notably `openai/widgetSessionId`, the
739
+ * ChatGPT MCP routing-bug workaround stamped by intent tools — see
740
+ * the descriptors.ts top-of-file comment) are preserved across all
741
+ * three modes. Don't strip additional `_meta` keys here without
742
+ * checking `descriptors.ts` first.
737
743
  */
738
744
  declare function narratedToolResult(tool: IntentTool | string, data: BootstrapPayload, mode?: SolvaPayToolMode, baseMeta?: Record<string, unknown> | undefined): SolvaPayCallToolResult;
739
745
  /**
@@ -861,7 +867,7 @@ declare const SOLVAPAY_DEFAULT_CSP: Required<SolvaPayMcpCsp>;
861
867
  declare function mergeCsp(overrides: SolvaPayMcpCsp | undefined, apiBaseUrl?: string): Required<SolvaPayMcpCsp>;
862
868
 
863
869
  /**
864
- * `applyHideToolsByAudience(server, audiences)` — wraps the
870
+ * `applyHideToolsByAudience(server, audiences, options?)` — wraps the
865
871
  * `tools/list` request handler on an `@modelcontextprotocol/sdk`
866
872
  * `McpServer` so tool descriptors whose `_meta.audience` matches one
867
873
  * of the supplied values are filtered out of the response.
@@ -869,21 +875,50 @@ declare function mergeCsp(overrides: SolvaPayMcpCsp | undefined, apiBaseUrl?: st
869
875
  * The tools stay `enabled: true` on the server, so `tools/call` still
870
876
  * reaches their handlers — this helper only affects the `tools/list`
871
877
  * response shape. Use `['ui']` when deploying to a text-host MCP
872
- * client (Claude Desktop, MCPJam, ChatGPT connectors) that won't
878
+ * client (Claude Desktop, MCPJam, ChatGPT connector) that won't
873
879
  * embed the SolvaPay iframe surface, while still allowing the iframe
874
880
  * to invoke the hidden transport tools (`create_payment_intent` etc.)
875
881
  * for server-side work.
876
882
  *
877
- * Internal-map reach-in rationale: `Protocol.setRequestHandler`
878
- * contains an `assertCanSetRequestHandler` guard that fails when the
879
- * SDK has already registered a handler for the method during
880
- * registration i.e. `tools/list` after any tool registration. The
881
- * guard only fires through the public wrapper; the underlying
882
- * `_requestHandlers` map accepts a replacement silently. Touching the
883
- * map directly therefore sidesteps the guard without going through
884
- * public API surface that could change in a minor SDK bump. The one
885
- * piece of SDK-internal knowledge we live with until the SDK ships a
886
- * first-class "replace handler" affordance.
883
+ * # ChatGPT auto-bypass
884
+ *
885
+ * ChatGPT's Custom Connector gateway re-validates iframe-initiated
886
+ * `tools/call` against the cached `tools/list` catalog. A tool hidden
887
+ * from `tools/list` becomes uncallable from the embedded iframe and
888
+ * surfaces in the UI as `MCP error -32000: MCP Resource not found`.
889
+ *
890
+ * To keep the cleaner LLM-facing catalog on every other host while
891
+ * the iframe still works on ChatGPT, the default behaviour
892
+ * automatically returns the **full** unfiltered catalog when the
893
+ * incoming `tools/list` request originates from ChatGPT — detected by
894
+ * matching `request.headers['user-agent']` and the post-`initialize`
895
+ * `server.getClientVersion().name` against `/openai-mcp/i`. The first
896
+ * triggers on the discovery `tools/list` ChatGPT issues before
897
+ * `initialize` (so `getClientVersion()` is empty); the second is a
898
+ * defence-in-depth fallback for any future relay that strips the
899
+ * client UA.
900
+ *
901
+ * The User-Agent shape was confirmed live against ChatGPT's MCP
902
+ * runtime (`openai-mcp/1.0.0 (ChatGPT)` as of 2026-05). The pattern
903
+ * is intentionally broad so a UA bump to `openai-mcp/2.x` keeps
904
+ * working without code changes.
905
+ *
906
+ * Override the detection by passing `bypassWhen` — useful when a
907
+ * future iframe-capable host needs the same treatment, or when
908
+ * ChatGPT-served deployments want the LLM-narrow catalog regardless
909
+ * (`bypassWhen: () => false`).
910
+ *
911
+ * # Internal-map reach-in rationale
912
+ *
913
+ * `Protocol.setRequestHandler` contains an `assertCanSetRequestHandler`
914
+ * guard that fails when the SDK has already registered a handler for
915
+ * the method during registration — i.e. `tools/list` after any tool
916
+ * registration. The guard only fires through the public wrapper; the
917
+ * underlying `_requestHandlers` map accepts a replacement silently.
918
+ * Touching the map directly therefore sidesteps the guard without
919
+ * going through public API surface that could change in a minor SDK
920
+ * bump. The one piece of SDK-internal knowledge we live with until
921
+ * the SDK ships a first-class "replace handler" affordance.
887
922
  *
888
923
  * No-op when `audiences` is empty or falsy.
889
924
  *
@@ -892,7 +927,58 @@ declare function mergeCsp(overrides: SolvaPayMcpCsp | undefined, apiBaseUrl?: st
892
927
  * `@solvapay/mcp/fetch` (unified `createSolvaPayMcpFetch`) can apply
893
928
  * the same filter without each re-implementing the reach-in.
894
929
  */
895
- declare function applyHideToolsByAudience(server: unknown, audiences: readonly string[] | undefined): void;
930
+ /**
931
+ * Headers shape exposed by the MCP SDK's `RequestHandlerExtra.requestInfo`.
932
+ * Mirrors `IsomorphicHeaders` from the SDK without taking a type dep.
933
+ */
934
+ type IsomorphicHeaders = Record<string, string | string[] | undefined>;
935
+ /**
936
+ * Subset of `RequestHandlerExtra` (from `@modelcontextprotocol/sdk`)
937
+ * that the bypass predicate inspects. Typed loosely so we don't
938
+ * couple to the SDK's exact shape.
939
+ */
940
+ interface ApplyHideToolsByAudienceExtra {
941
+ requestInfo?: {
942
+ headers?: IsomorphicHeaders;
943
+ } | undefined;
944
+ [key: string]: unknown;
945
+ }
946
+ interface ApplyHideToolsByAudienceContext {
947
+ /** The MCP server instance the filter is being applied to. */
948
+ server: unknown;
949
+ /**
950
+ * The `RequestHandlerExtra` the SDK passed to the wrapped
951
+ * `tools/list` handler. May be undefined for non-HTTP transports
952
+ * (e.g. stdio) where there's no request information.
953
+ */
954
+ extra?: ApplyHideToolsByAudienceExtra;
955
+ }
956
+ type HideToolsByAudienceBypass = (ctx: ApplyHideToolsByAudienceContext) => boolean;
957
+ interface ApplyHideToolsByAudienceOptions {
958
+ /**
959
+ * When this predicate returns `true` for an incoming `tools/list`
960
+ * request, the audience filter is skipped and the full catalog is
961
+ * returned. Defaults to `defaultIsChatGptRequest` — see the file
962
+ * header for the rationale.
963
+ *
964
+ * Pass an integrator-supplied predicate to extend the bypass to
965
+ * another host (e.g. a future iframe-capable client), or `() =>
966
+ * false` to apply the filter unconditionally.
967
+ */
968
+ bypassWhen?: HideToolsByAudienceBypass;
969
+ }
970
+ /**
971
+ * Default `bypassWhen` — returns true when the incoming request looks
972
+ * like it's coming from ChatGPT's MCP runtime. Two signals:
973
+ *
974
+ * 1. HTTP `User-Agent` header (works on the pre-`initialize`
975
+ * discovery `tools/list`, so we have a signal even before the
976
+ * SDK's `getClientVersion()` is populated).
977
+ * 2. `server.getClientVersion()?.name` (covers any host that relays
978
+ * ChatGPT requests without forwarding the upstream UA).
979
+ */
980
+ declare function defaultIsChatGptRequest(ctx: ApplyHideToolsByAudienceContext): boolean;
981
+ declare function applyHideToolsByAudience(server: unknown, audiences: readonly string[] | undefined, options?: ApplyHideToolsByAudienceOptions): void;
896
982
 
897
983
  /**
898
984
  * `buildSolvaPayDescriptors(options)` — framework-neutral tool surface
@@ -905,6 +991,28 @@ declare function applyHideToolsByAudience(server: unknown, audiences: readonly s
905
991
  * mechanical difference: instead of calling `registerAppTool(server, ...)`,
906
992
  * we push `{ name, handler, ... }` onto a `tools[]` array the adapter
907
993
  * iterates.
994
+ *
995
+ * ----
996
+ *
997
+ * `_meta["openai/widgetSessionId"]` workaround. Every intent-tool
998
+ * response stamps a freshly-minted UUID on `_meta["openai/widgetSessionId"]`.
999
+ * This is a low-risk forward-looking workaround for the ChatGPT MCP
1000
+ * connector's stale `link_<id>` routing bug, where the host returns
1001
+ * `-32000 MCP Resource not found` on the second `tools/call` of a
1002
+ * session even though the call never reaches the server. A fresh UUID
1003
+ * per invocation gives the host a routing key that changes every call,
1004
+ * which the OpenAI Apps SDK community thread reports unsticks the
1005
+ * failure mode.
1006
+ *
1007
+ * Sources:
1008
+ * - https://community.openai.com/t/connector-tool-calls-generating-fresh-mcp-session-each-invocation/1364975
1009
+ * - https://github.com/openai/openai-apps-sdk-examples/issues/165
1010
+ * - https://developers.openai.com/apps-sdk/reference/ (`_meta` payload)
1011
+ * - openai/openai-apps-sdk-examples shopping_cart_python uses the
1012
+ * same `meta["openai/widgetSessionId"]` shape.
1013
+ *
1014
+ * Removable once the upstream bug ships a fix; safe on any host that
1015
+ * doesn't consume the key.
908
1016
  */
909
1017
 
910
1018
  /**
@@ -1196,4 +1304,4 @@ interface BuildAuthInfoFromBearerOptions extends McpBearerCustomerRefOptions {
1196
1304
  }
1197
1305
  declare function buildAuthInfoFromBearer(authorization?: string | null, options?: BuildAuthInfoFromBearerOptions): McpToolExtra['authInfo'] | null;
1198
1306
 
1199
- export { type BootstrapCustomer, type BootstrapMerchant, type BootstrapPayload, type BootstrapPlan, type BootstrapProduct, type BuildAuthInfoFromBearerOptions, type BuildBootstrapPayloadFn, type BuildPayableHandlerContext, type BuildSolvaPayDescriptorsOptions, type BuildSolvaPayRequestOptions, type ContentBlock, type CreateBuildBootstrapPayloadOptions, type CustomerSnapshot, DEFAULT_OAUTH_PATHS, type IntentTool, MCP_TOOL_NAMES, type McpAdapterOptions, McpBearerAuthError, type McpBearerCustomerRefOptions, type McpToolExtra, type McpToolName, NARRATORS, type NarratorOutput, type NudgeSpec, type OAuthAuthorizationServerOptions, type OAuthBridgePaths, OPEN_TOOL_FOR_VIEW, type PayableHandler, type PaywallToolResult, type PaywallToolResultContext, type ResponseContext, type ResponseOptions, type ResponseResult, SOLVAPAY_DEFAULT_CSP, SOLVAPAY_MCP_VIEW_KINDS, SOLVAPAY_OVERVIEW_MARKDOWN, SOLVAPAY_OVERVIEW_MIME_TYPE, SOLVAPAY_OVERVIEW_URI, type SolvaPayCallToolResult, type SolvaPayDescriptorBundle, type SolvaPayDocsResourceDescriptor, type SolvaPayMcpCsp, type SolvaPayMcpViewKind, type SolvaPayMerchantBranding, type SolvaPayPromptDescriptor, type SolvaPayPromptResult, type SolvaPayResourceDescriptor, type SolvaPayToolAnnotations, type SolvaPayToolDescriptor, type SolvaPayToolIcon, type SolvaPayToolMode, TOOL_FOR_VIEW, VIEW_FOR_OPEN_TOOL, VIEW_FOR_TOOL, applyHideToolsByAudience, balanceSummary, buildAuthInfoFromBearer, buildPayableHandler, buildSolvaPayDescriptors, buildSolvaPayPrompts, buildSolvaPayRequest, createBuildBootstrapPayload, decodeJwtPayload, defaultGetCustomerRef, deriveIcons, enrichPurchase, extractBearerToken, getCustomerRefFromBearerAuthHeader, getCustomerRefFromJwtPayload, getOAuthAuthorizationServerResponse, getOAuthProtectedResourceResponse, mergeCsp, narrateActivatePlan, narrateManageAccount, narrateTopup, narrateUpgrade, narratedToolResult, parseMode, paywallToolResult, previewJson, resolveOAuthPaths, toolErrorResult, toolResult, uiPlaceholder, withoutTrailingSlash };
1307
+ export { type ApplyHideToolsByAudienceContext, type ApplyHideToolsByAudienceExtra, type ApplyHideToolsByAudienceOptions, type BootstrapCustomer, type BootstrapMerchant, type BootstrapPayload, type BootstrapPlan, type BootstrapProduct, type BuildAuthInfoFromBearerOptions, type BuildBootstrapPayloadFn, type BuildPayableHandlerContext, type BuildSolvaPayDescriptorsOptions, type BuildSolvaPayRequestOptions, type ContentBlock, type CreateBuildBootstrapPayloadOptions, type CustomerSnapshot, DEFAULT_OAUTH_PATHS, type HideToolsByAudienceBypass, type IntentTool, MCP_TOOL_NAMES, type McpAdapterOptions, McpBearerAuthError, type McpBearerCustomerRefOptions, type McpToolExtra, type McpToolName, NARRATORS, type NarratorOutput, type NudgeSpec, type OAuthAuthorizationServerOptions, type OAuthBridgePaths, OPEN_TOOL_FOR_VIEW, type PayableHandler, type PaywallToolResult, type PaywallToolResultContext, type ResponseContext, type ResponseOptions, type ResponseResult, SOLVAPAY_DEFAULT_CSP, SOLVAPAY_MCP_VIEW_KINDS, SOLVAPAY_OVERVIEW_MARKDOWN, SOLVAPAY_OVERVIEW_MIME_TYPE, SOLVAPAY_OVERVIEW_URI, type SolvaPayCallToolResult, type SolvaPayDescriptorBundle, type SolvaPayDocsResourceDescriptor, type SolvaPayMcpCsp, type SolvaPayMcpViewKind, type SolvaPayMerchantBranding, type SolvaPayPromptDescriptor, type SolvaPayPromptResult, type SolvaPayResourceDescriptor, type SolvaPayToolAnnotations, type SolvaPayToolDescriptor, type SolvaPayToolIcon, type SolvaPayToolMode, TOOL_FOR_VIEW, VIEW_FOR_OPEN_TOOL, VIEW_FOR_TOOL, applyHideToolsByAudience, balanceSummary, buildAuthInfoFromBearer, buildPayableHandler, buildSolvaPayDescriptors, buildSolvaPayPrompts, buildSolvaPayRequest, createBuildBootstrapPayload, decodeJwtPayload, defaultGetCustomerRef, defaultIsChatGptRequest, deriveIcons, enrichPurchase, extractBearerToken, getCustomerRefFromBearerAuthHeader, getCustomerRefFromJwtPayload, getOAuthAuthorizationServerResponse, getOAuthProtectedResourceResponse, mergeCsp, narrateActivatePlan, narrateManageAccount, narrateTopup, narrateUpgrade, narratedToolResult, parseMode, paywallToolResult, previewJson, resolveOAuthPaths, toolErrorResult, toolResult, uiPlaceholder, withoutTrailingSlash };
package/dist/index.js CHANGED
@@ -424,9 +424,24 @@ function mergeCsp(overrides, apiBaseUrl) {
424
424
  }
425
425
 
426
426
  // src/hideToolsByAudience.ts
427
- function applyHideToolsByAudience(server, audiences) {
427
+ var CHATGPT_CLIENT_RE = /openai-mcp/i;
428
+ function readHeader(headers, name) {
429
+ if (!headers) return void 0;
430
+ const raw = headers[name] ?? headers[name.toLowerCase()];
431
+ if (Array.isArray(raw)) return raw[0];
432
+ return typeof raw === "string" ? raw : void 0;
433
+ }
434
+ function defaultIsChatGptRequest(ctx) {
435
+ const ua = readHeader(ctx.extra?.requestInfo?.headers, "user-agent");
436
+ if (ua && CHATGPT_CLIENT_RE.test(ua)) return true;
437
+ const clientVersion = ctx.server?.server?.getClientVersion?.();
438
+ const clientName = typeof clientVersion?.name === "string" ? clientVersion.name : void 0;
439
+ return clientName !== void 0 && CHATGPT_CLIENT_RE.test(clientName);
440
+ }
441
+ function applyHideToolsByAudience(server, audiences, options = {}) {
428
442
  if (!audiences || audiences.length === 0) return;
429
443
  const hidden = new Set(audiences);
444
+ const bypassWhen = options.bypassWhen ?? defaultIsChatGptRequest;
430
445
  const inner = server.server;
431
446
  if (!inner || typeof inner !== "object" || !(inner._requestHandlers instanceof Map)) {
432
447
  return;
@@ -434,8 +449,26 @@ function applyHideToolsByAudience(server, audiences) {
434
449
  const handlers = inner._requestHandlers;
435
450
  const original = handlers.get("tools/list");
436
451
  if (!original) return;
452
+ const warned = /* @__PURE__ */ new Set();
437
453
  handlers.set("tools/list", async (req, extra) => {
438
454
  const res = await original(req, extra);
455
+ if (bypassWhen({
456
+ server,
457
+ extra
458
+ })) {
459
+ const ua = readHeader(
460
+ extra?.requestInfo?.headers,
461
+ "user-agent"
462
+ );
463
+ const context = ua ? `ua=${ua}` : "no user-agent";
464
+ if (!warned.has(context)) {
465
+ warned.add(context);
466
+ console.warn(
467
+ `[solvapay/mcp] hideToolsByAudience filter bypassed (${context}); returning full tools/list catalog.`
468
+ );
469
+ }
470
+ return res;
471
+ }
439
472
  const tools = Array.isArray(res?.tools) ? res.tools : [];
440
473
  return {
441
474
  ...res,
@@ -706,7 +739,10 @@ function buildSolvaPayDescriptors(options) {
706
739
  handler: async (args, extra) => trace(name, args, extra, async () => {
707
740
  const mode = parseMode(args.mode);
708
741
  const data = await buildBootstrapPayload(view, extra);
709
- return narratedToolResult(name, data, mode, toolMeta);
742
+ return narratedToolResult(name, data, mode, {
743
+ ...toolMeta,
744
+ "openai/widgetSessionId": crypto.randomUUID()
745
+ });
710
746
  })
711
747
  });
712
748
  };
@@ -907,7 +943,7 @@ function buildSolvaPayDescriptors(options) {
907
943
  MCP_TOOL_NAMES.activatePlan,
908
944
  await buildBootstrapPayload("checkout", extra),
909
945
  mode,
910
- toolMeta
946
+ { ...toolMeta, "openai/widgetSessionId": crypto.randomUUID() }
911
947
  );
912
948
  }
913
949
  const auth = requireCustomerRef(extra);
@@ -1316,6 +1352,7 @@ export {
1316
1352
  createBuildBootstrapPayload,
1317
1353
  decodeJwtPayload,
1318
1354
  defaultGetCustomerRef,
1355
+ defaultIsChatGptRequest,
1319
1356
  deriveIcons,
1320
1357
  enrichPurchase,
1321
1358
  extractBearerToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solvapay/mcp-core",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Framework-neutral MCP contracts for the SolvaPay SDK (tool names, descriptors, payable handler, paywall meta, CSP, bootstrap payload, OAuth discovery JSON builders, bearer/JWT helpers).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",