@apiosk/mcp 1.3.1 → 2.0.0

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 (92) hide show
  1. package/README.md +84 -537
  2. package/assets/brand/apiosk-a-20260921.png +0 -0
  3. package/assets/brand/apple-touch-icon.png +0 -0
  4. package/assets/brand/favicon-dark.ico +0 -0
  5. package/assets/brand/favicon-light.ico +0 -0
  6. package/assets/brand/favicon.ico +0 -0
  7. package/assets/brand/icon-192.png +0 -0
  8. package/assets/brand/icon-512.png +0 -0
  9. package/assets/brand/icon-maskable-512.png +0 -0
  10. package/assets/brand/inter-OFL.txt +93 -0
  11. package/assets/brand/inter-latin-400-normal.woff2 +0 -0
  12. package/assets/brand/inter-latin-500-normal.woff2 +0 -0
  13. package/assets/brand/inter-latin-600-normal.woff2 +0 -0
  14. package/assets/brand/mark-20260905-transparent.svg +1 -0
  15. package/assets/brand/mark-20260918.svg +1 -0
  16. package/assets/brand/mark-black-20260918.svg +1 -0
  17. package/assets/brand/mark-dark-20260905-transparent.png +0 -0
  18. package/assets/brand/mark-dark-20260918.png +0 -0
  19. package/assets/brand/mark-dark-96.png +0 -0
  20. package/assets/brand/mark-light-20260905-transparent.png +0 -0
  21. package/assets/brand/mark-light-20260918.png +0 -0
  22. package/assets/brand/mark-light-96.png +0 -0
  23. package/assets/brand/wordmark-black-320.png +0 -0
  24. package/assets/brand/wordmark-white-320.png +0 -0
  25. package/docs/branding.md +11 -0
  26. package/docs/marketplace-submission-2026-04-08.md +4 -0
  27. package/docs/marketplace-submission-2026-08-20.md +157 -0
  28. package/docs/openai-plugin-submission-2026-09-05.md +143 -0
  29. package/docs/sepa-rail.md +13 -13
  30. package/dxt.json +26 -22
  31. package/index.mjs +3 -1
  32. package/logo-optimized-light.png +0 -0
  33. package/package.json +15 -13
  34. package/plugin/apiosk/.codex-plugin/plugin.json +45 -0
  35. package/plugin/apiosk/.mcp.json +8 -0
  36. package/plugin/apiosk/assets/icon-dark.png +0 -0
  37. package/plugin/apiosk/assets/icon.png +0 -0
  38. package/plugin/apiosk/assets/icon.svg +1 -0
  39. package/plugin/apiosk/assets/logo.png +0 -0
  40. package/plugin/apiosk/skills/apiosk/SKILL.md +33 -0
  41. package/plugin/apiosk/skills/apiosk/agents/openai.yaml +12 -0
  42. package/server.json +98 -9
  43. package/server.mjs +258 -71
  44. package/src/approval-feedback.mjs +21 -0
  45. package/src/brand-routes.mjs +28 -0
  46. package/src/create-server.mjs +168 -9
  47. package/src/display-money.mjs +20 -0
  48. package/src/display-text.mjs +67 -0
  49. package/src/gateway-client.mjs +42 -0
  50. package/src/gateway-v2-ask.mjs +50 -0
  51. package/src/gateway-v2-card-account.mjs +23 -0
  52. package/src/gateway-v2-card-actions.mjs +67 -0
  53. package/src/gateway-v2-card-answer-text.mjs +130 -0
  54. package/src/gateway-v2-card-answer.mjs +68 -0
  55. package/src/gateway-v2-card-blocks.mjs +166 -0
  56. package/src/gateway-v2-card-body.mjs +259 -0
  57. package/src/gateway-v2-card-budget.mjs +21 -0
  58. package/src/gateway-v2-card-cbs.mjs +56 -0
  59. package/src/gateway-v2-card-choices.mjs +4 -0
  60. package/src/gateway-v2-card-clarification.mjs +25 -0
  61. package/src/gateway-v2-card-compact.mjs +34 -0
  62. package/src/gateway-v2-card-events.mjs +41 -0
  63. package/src/gateway-v2-card-presentation.mjs +125 -0
  64. package/src/gateway-v2-card-research.mjs +63 -0
  65. package/src/gateway-v2-card-result.mjs +32 -0
  66. package/src/gateway-v2-card-search.mjs +39 -0
  67. package/src/gateway-v2-card-sources.mjs +32 -0
  68. package/src/gateway-v2-card-style.mjs +76 -0
  69. package/src/gateway-v2-card-verdict.mjs +66 -0
  70. package/src/gateway-v2-card.mjs +91 -0
  71. package/src/gateway-v2-contracts.json +66 -0
  72. package/src/gateway-v2-instructions.md +113 -0
  73. package/src/gateway-v2-recovery.mjs +18 -0
  74. package/src/gateway-v2-report-links.mjs +14 -0
  75. package/src/gateway-v2-workflows.mjs +17 -0
  76. package/src/gateway-v2.mjs +179 -0
  77. package/src/oauth.mjs +603 -364
  78. package/src/observability.mjs +210 -0
  79. package/src/result-presentation.mjs +9 -0
  80. package/src/runtime.mjs +24 -3091
  81. package/src/settlement-disclosure.mjs +26 -0
  82. package/src/source-groups.mjs +91 -0
  83. package/src/source-value-format.mjs +25 -0
  84. package/src/tool-result.mjs +20 -0
  85. package/src/ui-bridge.mjs +185 -0
  86. package/src/well-known-routes.mjs +132 -0
  87. package/src/funding-options.mjs +0 -255
  88. package/src/gateway-management.mjs +0 -107
  89. package/src/listing-metadata.mjs +0 -287
  90. package/src/local-config.mjs +0 -256
  91. package/src/payment-guidance.mjs +0 -307
  92. package/src/wallet-store.mjs +0 -476
package/server.json CHANGED
@@ -1,26 +1,97 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.obcraft/apiosk-mcp",
4
- "title": "Apiosk",
5
- "description": "AI-native payments: discover, pay for, execute, and publish monetized APIs through MCP.",
6
- "version": "1.3.1",
4
+ "title": "Apiosk Connect",
5
+ "description": "Ask a data question, review one plan and total price ceiling, approve in the chat card within your connected account's spending limits, and receive source-backed results. Resume without buying the same work twice.",
6
+ "version": "2.0.0",
7
7
  "websiteUrl": "https://apiosk.com",
8
8
  "icons": [
9
9
  {
10
- "src": "https://apiosk.com/logo.svg",
10
+ "src": "https://mcp.apiosk.com/brand/apiosk-a-20260921.png",
11
+ "mimeType": "image/png",
12
+ "sizes": [
13
+ "1254x1254"
14
+ ],
15
+ "theme": "light"
16
+ },
17
+ {
18
+ "src": "https://mcp.apiosk.com/brand/mark-20260918.svg",
11
19
  "mimeType": "image/svg+xml",
12
- "sizes": ["any"]
20
+ "sizes": [
21
+ "any"
22
+ ]
23
+ },
24
+ {
25
+ "src": "https://mcp.apiosk.com/brand/mark-light-20260918.png",
26
+ "mimeType": "image/png",
27
+ "sizes": [
28
+ "512x512"
29
+ ],
30
+ "theme": "light"
13
31
  },
14
32
  {
15
- "src": "https://apiosk.com/apple-touch-icon.png",
33
+ "src": "https://mcp.apiosk.com/brand/mark-dark-20260918.png",
16
34
  "mimeType": "image/png",
17
- "sizes": ["180x180"]
35
+ "sizes": [
36
+ "512x512"
37
+ ],
38
+ "theme": "dark"
18
39
  }
19
40
  ],
20
41
  "repository": {
21
42
  "url": "https://github.com/obcraft/apiosk-mcp",
22
43
  "source": "github"
23
44
  },
45
+ "packages": [
46
+ {
47
+ "registryType": "npm",
48
+ "identifier": "@apiosk/mcp",
49
+ "version": "2.0.0",
50
+ "transport": {
51
+ "type": "stdio"
52
+ },
53
+ "environmentVariables": [
54
+ {
55
+ "name": "APIOSK_CONNECT_TOKEN",
56
+ "description": "Apiosk agent token for stdio (apk_access_… from a connection, or a legacy apk_live_… agent key). Hosted MCP uses OAuth instead. Every tool acts for a connected account.",
57
+ "isRequired": true,
58
+ "isSecret": true,
59
+ "format": "string"
60
+ },
61
+ {
62
+ "name": "APIOSK_GATEWAY_V2_URL",
63
+ "description": "Gateway v2 origin. Defaults to https://gateway.apiosk.com; set only for a local or staging gateway.",
64
+ "isRequired": false,
65
+ "isSecret": false,
66
+ "format": "string"
67
+ }
68
+ ]
69
+ },
70
+ {
71
+ "registryType": "pypi",
72
+ "identifier": "apiosk-mcp",
73
+ "version": "2.0.0",
74
+ "transport": {
75
+ "type": "stdio"
76
+ },
77
+ "environmentVariables": [
78
+ {
79
+ "name": "APIOSK_CONNECT_TOKEN",
80
+ "description": "Apiosk agent token for stdio (apk_access_… from a connection, or a legacy apk_live_… agent key). Hosted MCP uses OAuth instead. Every tool acts for a connected account.",
81
+ "isRequired": true,
82
+ "isSecret": true,
83
+ "format": "string"
84
+ },
85
+ {
86
+ "name": "APIOSK_GATEWAY_V2_URL",
87
+ "description": "Gateway v2 origin. Defaults to https://gateway.apiosk.com; set only for a local or staging gateway.",
88
+ "isRequired": false,
89
+ "isSecret": false,
90
+ "format": "string"
91
+ }
92
+ ]
93
+ }
94
+ ],
24
95
  "remotes": [
25
96
  {
26
97
  "type": "streamable-http",
@@ -29,8 +100,26 @@
29
100
  ],
30
101
  "_meta": {
31
102
  "com.apiosk": {
32
- "tags": ["payments", "finance", "x402", "commerce", "crypto"],
33
- "categories": ["payments", "finance"]
103
+ "tags": [
104
+ "payments",
105
+ "finance",
106
+ "x402",
107
+ "commerce",
108
+ "crypto"
109
+ ],
110
+ "categories": [
111
+ "payments",
112
+ "finance"
113
+ ],
114
+ "tools": [
115
+ "apiosk_sources",
116
+ "apiosk_discover",
117
+ "apiosk_search",
118
+ "apiosk_prepare",
119
+ "apiosk_execute",
120
+ "apiosk_status",
121
+ "apiosk_approve"
122
+ ]
34
123
  }
35
124
  }
36
125
  }
package/server.mjs CHANGED
@@ -1,16 +1,31 @@
1
+ import { registerBrandRoutes, BRAND_LINKS } from "./src/brand-routes.mjs";
1
2
  import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
3
+ import { fileURLToPath } from "node:url";
2
4
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
3
6
  import {
4
- SERVER_INFO,
7
+ resolveServerPresentation,
5
8
  createApioskMcpServer,
6
9
  listApioskTools,
7
10
  } from "./src/create-server.mjs";
8
- import { createHostedOAuthSupport, resolveHostedMcpUrls } from "./src/oauth.mjs";
11
+ import { V2_RESOURCE } from "./src/gateway-v2.mjs";
12
+ import {
13
+ createHostedOAuthSupport,
14
+ resolveHostedMcpUrls,
15
+ } from "./src/oauth.mjs";
9
16
  import { createApioskMcpRuntime } from "./src/runtime.mjs";
17
+ import { openSession, closeSession } from "./src/observability.mjs";
10
18
  import {
11
19
  resolveOpenAiAppsChallengeToken,
12
20
  sendOpenAiAppsChallenge,
13
21
  } from "./well-known.mjs";
22
+ import { buildDiscoveryDocument } from "./src/well-known-routes.mjs";
23
+ import {
24
+ SETTLEMENT_DISCLOSURE_PATH,
25
+ createSettlementDisclosurePage,
26
+ } from "./src/settlement-disclosure.mjs";
27
+
28
+ const { info: SERVER_INFO, description: SERVER_DESCRIPTION, instructions: SERVER_INSTRUCTIONS } = resolveServerPresentation();
14
29
 
15
30
  const CONTROL_PLANE_BACKEND_URL = (
16
31
  process.env.APIOSK_CONTROL_PLANE_BACKEND_URL ||
@@ -46,10 +61,11 @@ function normalizeControlPlanePath(pathname = "") {
46
61
  }
47
62
 
48
63
  function shouldProxyControlPlanePath(pathname = "") {
64
+ // Hosted sign-in happens entirely at buy.apiosk.com now (see
65
+ // mcp/01-portal-handoff.md); this server neither renders a sign-in page nor
66
+ // proxies auth routes to the control plane.
49
67
  const normalizedPath = normalizeControlPlanePath(pathname);
50
68
  return (
51
- normalizedPath === "/api/auth/mcp-sign-in" ||
52
- normalizedPath === "/api/auth/mcp-sign-up" ||
53
69
  normalizedPath.startsWith("/api/credits/") ||
54
70
  normalizedPath === "/api/agent-wallets" ||
55
71
  normalizedPath.startsWith("/api/agent-wallets/")
@@ -111,75 +127,41 @@ function renderMcpWelcomeHtml(mcpUrl) {
111
127
  <meta charset="utf-8" />
112
128
  <meta name="viewport" content="width=device-width, initial-scale=1" />
113
129
  <meta name="robots" content="noindex" />
114
- <title>Apiosk MCP</title>
130
+ <title>Apiosk Connect</title>
131
+ ${BRAND_LINKS}
115
132
  <style>
116
- :root { color-scheme: dark; }
117
- * { box-sizing: border-box; }
118
- body {
119
- margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
120
- background: radial-gradient(1200px 600px at 50% -10%, #1b2230, #0b0e14 60%);
121
- color: #e6e9ef; font: 15px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
122
- padding: 32px;
123
- }
124
- main { max-width: 640px; width: 100%; }
125
- .badge {
126
- display: inline-block; font-size: 12px; letter-spacing: .08em; text-transform: uppercase;
127
- color: #8aa0c6; border: 1px solid #2a3344; border-radius: 999px; padding: 4px 12px; margin-bottom: 20px;
128
- }
129
- h1 { font-size: 30px; margin: 0 0 12px; letter-spacing: -.02em; }
130
- p { color: #b8c0cf; margin: 0 0 16px; }
131
- .lead { color: #d6dce6; font-size: 17px; }
132
- ul { color: #b8c0cf; margin: 0 0 16px; padding-left: 20px; }
133
- li { margin: 6px 0; }
134
- code {
135
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;
136
- background: #141a24; border: 1px solid #232c3a; border-radius: 6px; padding: 2px 7px; color: #cfe0ff;
137
- }
138
- .card { background: #0f141c; border: 1px solid #1f2937; border-radius: 14px; padding: 28px 30px; }
139
- .endpoint {
140
- display: block; margin: 4px 0 20px; padding: 12px 14px; background: #141a24;
141
- border: 1px solid #232c3a; border-radius: 8px; color: #cfe0ff; word-break: break-all;
142
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;
143
- }
144
- h2 { font-size: 13px; letter-spacing: .06em; text-transform: uppercase; color: #7f8aa0; margin: 24px 0 8px; }
145
- a { color: #7aa2ff; text-decoration: none; }
146
- a:hover { text-decoration: underline; }
147
- .links { display: flex; gap: 18px; flex-wrap: wrap; margin-top: 18px; padding-top: 18px; border-top: 1px solid #1f2937; }
148
- footer { color: #6b7689; font-size: 12px; margin-top: 20px; }
133
+ @font-face{font-family:Inter;src:url("/brand/inter-latin-500-normal.woff2") format("woff2");font-weight:500;font-display:swap}@font-face{font-family:Inter;src:url("/brand/inter-latin-600-normal.woff2") format("woff2");font-weight:600;font-display:swap}
134
+ :root{color-scheme:light dark;--bg:#f8f8fb;--card:#fff;--text:#1f2028;--muted:#676371;--border:#e7e3ef;--soft:#f2f1f6;--accent:#6349db;--wash:rgb(99 73 219/.07)}
135
+ @media(prefers-color-scheme:dark){:root{--bg:#0d0f13;--card:#15171d;--text:#ecebf2;--muted:#a5a2b0;--border:#262a34;--soft:#1d2028;--accent:#c3a0ff;--wash:rgb(195 160 255/.12)}}
136
+ *{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;background:radial-gradient(760px 340px at 50% -12%,var(--wash),transparent 70%),var(--bg);color:var(--text);font:500 14px/1.5 Inter,ui-sans-serif,system-ui,sans-serif;letter-spacing:-.011em;padding:24px;-webkit-font-smoothing:antialiased}
137
+ main{max-width:560px;width:100%}.card{background:var(--card);border:1px solid var(--border);border-radius:18px;padding:26px;box-shadow:0 18px 45px -38px rgba(72,42,145,.45)}
138
+ .brand{display:block;width:92px;height:30px;margin-bottom:24px}.brand img{display:block;width:92px;height:30px;object-fit:contain;object-position:left center}
139
+ .badge{display:inline-block;font-size:10px;color:var(--muted);border:1px solid var(--border);border-radius:999px;padding:4px 8px;margin-bottom:12px}
140
+ h1{font-size:25px;font-weight:600;line-height:1.18;margin:0 0 8px;letter-spacing:-.032em}p{color:var(--muted);margin:0}.lead{font-size:14px}
141
+ .steps{display:grid;gap:8px;margin:20px 0}.step{display:flex;align-items:center;gap:10px;padding:10px 11px;background:var(--soft);border-radius:11px}.step b{display:grid;place-items:center;width:22px;height:22px;border-radius:7px;background:var(--wash);color:var(--accent);font-size:10px}.step span{font-size:12px}
142
+ h2{font-size:11px;font-weight:600;margin:18px 0 7px}.endpoint{display:block;padding:10px 11px;background:var(--soft);border:1px solid var(--border);border-radius:10px;color:var(--text);word-break:break-all;font:500 11px/1.5 Inter,ui-sans-serif,system-ui,sans-serif}
143
+ details{margin-top:12px;color:var(--muted);font-size:12px}details code{display:block;margin-top:8px;padding:9px 10px;background:var(--soft);border-radius:8px;word-break:break-all}.links{display:flex;gap:15px;flex-wrap:wrap;margin-top:20px;padding-top:16px;border-top:1px solid var(--border)}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}footer{color:var(--muted);font-size:10px;margin-top:14px}
149
144
  </style>
150
145
  </head>
151
146
  <body>
152
147
  <main>
153
148
  <div class="card">
149
+ <picture class="brand"><source media="(prefers-color-scheme:dark)" srcset="/brand/wordmark-white-320.png"><img src="/brand/wordmark-black-320.png" alt="Apiosk" width="320" height="103"></picture>
154
150
  <span class="badge">Model Context Protocol</span>
155
- <h1>Welcome to Apiosk MCP</h1>
156
- <p class="lead">
157
- This is the Apiosk MCP server endpoint &mdash; it lets AI agents discover, pay for,
158
- execute, and publish APIs through the Apiosk gateway. It is a machine endpoint, not a
159
- website, so connect it from an MCP client (Claude, Cursor, ChatGPT, and others) rather
160
- than browsing it here.
161
- </p>
162
-
163
- <h2>What you can do with it</h2>
164
- <ul>
165
- <li><strong>Discover</strong> APIs, datasets, and services in the Apiosk catalog.</li>
166
- <li><strong>Pay</strong> per call automatically in USDC (x402 on Base).</li>
167
- <li><strong>Execute</strong> any listing through a uniform contract or its API-specific tool.</li>
168
- <li><strong>Publish &amp; manage</strong> your own APIs so other agents can find and pay for them.</li>
169
- </ul>
170
-
171
- <h2>Endpoint</h2>
172
- <code class="endpoint">${mcpUrl}</code>
151
+ <h1>Connect Apiosk to your chatbot</h1>
152
+ <p class="lead">Ask for specialist data, approve the total price in Apiosk and receive the source-backed result in the same conversation.</p>
153
+ <div class="steps"><div class="step"><b>1</b><span>Ask a data question</span></div><div class="step"><b>2</b><span>Review and approve the price</span></div><div class="step"><b>3</b><span>Receive the result and source JSON</span></div></div>
173
154
 
174
- <h2>Connect from Claude Code</h2>
175
- <code>claude mcp add --transport http apiosk ${mcpUrl}</code>
155
+ <h2>Secure MCP endpoint</h2>
156
+ <code class="endpoint">${mcpUrl}</code>
157
+ <details><summary>Manual setup</summary><code>claude mcp add --transport http apiosk ${mcpUrl}</code><code>${mcpUrl.replace(/\/mcp$/, "/sse")}</code></details>
176
158
 
177
159
  <div class="links">
178
160
  <a href="/health">Health</a>
179
161
  <a href="https://dashboard.apiosk.com" target="_blank" rel="noopener">Dashboard</a>
180
162
  <a href="https://github.com/obcraft/apiosk-mcp" target="_blank" rel="noopener">Docs &amp; source</a>
181
163
  </div>
182
- <footer>Apiosk MCP &middot; ${SERVER_INFO.name} v${SERVER_INFO.version}</footer>
164
+ <footer>Apiosk Connect &middot; ${SERVER_INFO.name} v${SERVER_INFO.version}</footer>
183
165
  </div>
184
166
  </main>
185
167
  </body>
@@ -199,12 +181,12 @@ function sendMcpWelcome(req, res) {
199
181
  }
200
182
 
201
183
  res.status(200).json({
202
- name: "Apiosk MCP",
184
+ name: "Apiosk Connect",
203
185
  server: SERVER_INFO,
204
- description:
205
- "Apiosk MCP server endpoint. Connect it from an MCP client to discover, pay for, execute, and publish APIs through the Apiosk gateway.",
186
+ description: SERVER_DESCRIPTION,
206
187
  transport: "streamable-http",
207
188
  endpoint: mcpUrl,
189
+ legacy_sse_endpoint: mcpUrl.replace(/\/mcp$/, "/sse"),
208
190
  connect: {
209
191
  claude_code: `claude mcp add --transport http apiosk ${mcpUrl}`,
210
192
  },
@@ -215,26 +197,48 @@ function sendMcpWelcome(req, res) {
215
197
 
216
198
  // Public Fly deployment must accept the Fly hostname instead of localhost-only
217
199
  // host validation defaults.
218
- const app = createMcpExpressApp({ host: "0.0.0.0" });
200
+ const bindHost = process.env.APIOSK_MCP_BIND_HOST || "0.0.0.0";
201
+ const app = createMcpExpressApp({ host: bindHost });
202
+ // Fly terminates TLS and adds one trusted proxy hop. Without this,
203
+ // express-rate-limit rejects X-Forwarded-For and can surface as MCP -32603.
204
+ app.set("trust proxy", 1);
219
205
  const port = Number(process.env.PORT || 3000);
220
206
  const { issuerUrl, mcpServerUrl } = resolveHostedMcpUrls({
221
207
  env: process.env,
222
208
  port,
223
209
  });
224
210
  const runtime = createApioskMcpRuntime({
225
- enableLocalWallets: process.env.APIOSK_ENABLE_LOCAL_WALLETS === "true",
211
+ // Hosted MCP is multi-tenant: the buyer's identity arrives on each request
212
+ // through OAuth and never falls back to a machine-wide token.
226
213
  hostedAuthEnabled: true,
227
214
  });
228
215
  const hostedOAuth = createHostedOAuthSupport({
229
216
  env: process.env,
230
- controlPlaneBaseUrl: CONTROL_PLANE_BACKEND_URL,
231
217
  issuerUrl,
232
218
  mcpServerUrl,
233
219
  appName: "Apiosk",
234
- resourceName: "Apiosk MCP",
220
+ resourceName: "Apiosk Connect",
235
221
  });
236
222
  const mcpAuthMiddleware = hostedOAuth.createMcpAuthMiddleware(runtime);
237
223
 
224
+ // Operational evidence without buyer prompts, credentials, OAuth codes or
225
+ // result bodies. In particular distinguish a host timeout from an auth error.
226
+ app.use((req, res, next) => {
227
+ const path = req.path;
228
+ if (!["/mcp", "/messages", "/token", "/authorize", "/register", "/.well-known/oauth-authorization-server", "/.well-known/oauth-protected-resource/mcp"].includes(path)) return next();
229
+ const started = Date.now();
230
+ res.once("finish", () => console.info(JSON.stringify({
231
+ event: "mcp_request", path, method: req.method,
232
+ rpc_method: ["initialize", "tools/list", "tools/call", "resources/list", "resources/read", "notifications/initialized"].includes(req.body?.method) ? req.body.method : undefined,
233
+ client_name: req.body?.method === "initialize" && /^[a-zA-Z0-9 ._/-]{1,80}$/.test(req.body?.params?.clientInfo?.name || "") ? req.body.params.clientInfo.name : undefined,
234
+ client_family: /openai|chatgpt/i.test(req.headers['user-agent'] || '') ? 'openai' : /claude|anthropic/i.test(req.headers['user-agent'] || '') ? 'anthropic' : 'other',
235
+ has_bearer: /^Bearer /i.test(req.headers.authorization || ''),
236
+ ui_resource: req.body?.method === 'resources/read' && /^ui:\/\/apiosk\/gateway-v2-card-v[0-9]+(?:-chatgpt)?\.html$/.test(req.body?.params?.uri || '') ? req.body.params.uri : undefined,
237
+ status: res.statusCode, duration_ms: Date.now() - started,
238
+ })));
239
+ next();
240
+ });
241
+
238
242
  app.use(hostedOAuth.metadataRouter);
239
243
  app.use(new URL(hostedOAuth.oauthMetadata.authorization_endpoint).pathname, hostedOAuth.authorizationRouter);
240
244
  app.use(new URL(hostedOAuth.oauthMetadata.token_endpoint).pathname, hostedOAuth.tokenRouter);
@@ -244,11 +248,62 @@ if (hostedOAuth.oauthMetadata.registration_endpoint) {
244
248
  hostedOAuth.registrationRouter
245
249
  );
246
250
  }
251
+ // buy.apiosk.com's return leg for the portal handoff (mcp/01-portal-handoff.md).
252
+ app.get("/authorize/callback", (req, res) => hostedOAuth.handlePortalCallback(req, res));
247
253
 
248
254
  app.get(OPENAI_APPS_CHALLENGE_PATH_PATTERN, (req, res) => {
249
255
  return sendOpenAiAppsChallenge(res, OPENAI_APPS_CHALLENGE_TOKEN);
250
256
  });
251
257
 
258
+ // Public favicon discovery complements MCP initialize metadata for hosts
259
+ // that resolve connector branding from the server's origin.
260
+ registerBrandRoutes(app);
261
+ app.get("/", (req, res) => {
262
+ res.setHeader("cache-control", "public, max-age=0, must-revalidate");
263
+ return sendMcpWelcome(req, res);
264
+ });
265
+
266
+ // Preserve the original URL for already-installed connectors.
267
+ app.get("/logo-optimized-light.png", (req, res) => {
268
+ res.setHeader("cache-control", "public, max-age=0, must-revalidate");
269
+ res.type("png").sendFile(fileURLToPath(new URL("./logo-optimized-light.png", import.meta.url)));
270
+ });
271
+
272
+ // Only the declared, versioned brand files are exposed.
273
+ for (const icon of SERVER_INFO.icons) {
274
+ const pathname = new URL(icon.src).pathname;
275
+ const filename = pathname.slice("/brand/".length);
276
+ app.get(pathname, (_req, res) => {
277
+ res.setHeader("cache-control", "public, max-age=31536000, immutable");
278
+ res.type(icon.mimeType).sendFile(fileURLToPath(new URL(`./assets/brand/${filename}`, import.meta.url)));
279
+ });
280
+ }
281
+
282
+ // UI resources use the same compact wordmark and self-hosted Inter weights as
283
+ // the App. Keep this an explicit allowlist: /brand is not a general file
284
+ // server, and adding a packaged asset does not make it public by accident.
285
+ for (const asset of [
286
+ { filename: "wordmark-black-320.png", type: "image/png" },
287
+ { filename: "wordmark-white-320.png", type: "image/png" },
288
+ { filename: "inter-latin-400-normal.woff2", type: "font/woff2" },
289
+ { filename: "inter-latin-500-normal.woff2", type: "font/woff2" },
290
+ { filename: "inter-latin-600-normal.woff2", type: "font/woff2" },
291
+ ]) {
292
+ app.get(`/brand/${asset.filename}`, (_req, res) => {
293
+ res.setHeader("cache-control", "public, max-age=31536000, immutable");
294
+ res.type(asset.type).sendFile(
295
+ fileURLToPath(new URL(`./assets/brand/${asset.filename}`, import.meta.url))
296
+ );
297
+ });
298
+ }
299
+
300
+ app.get(SETTLEMENT_DISCLOSURE_PATH, (_req, res) => {
301
+ res
302
+ .setHeader("cache-control", "public, max-age=300")
303
+ .type("html")
304
+ .send(createSettlementDisclosurePage());
305
+ });
306
+
252
307
  app.all("/api/*path", async (req, res) => {
253
308
  try {
254
309
  const pathname = req.path || req.originalUrl || "";
@@ -270,6 +325,70 @@ app.all("/api/*path", async (req, res) => {
270
325
  }
271
326
  });
272
327
 
328
+ // Static server card (SEP-1649). Lets a registry read this server's identity,
329
+ // auth model and capabilities without opening an MCP session.
330
+ //
331
+ // Built from the SAME runtime that answers tools/list rather than from a
332
+ // hand-written literal, because a card that disagrees with the live server is
333
+ // worse than no card: a scanner would publish a tool list nobody can call.
334
+ //
335
+ // `authentication.required` is true: every Gateway v2 tool acts for a
336
+ // connected Apiosk account, so hosts configure OAuth when they connect.
337
+ app.get("/.well-known/mcp/server-card.json", async (req, res) => {
338
+ try {
339
+ const tools = await listApioskTools({ hostedAuthEnabled: true });
340
+ res.setHeader("cache-control", "public, max-age=300");
341
+ res.json({
342
+ serverInfo: SERVER_INFO,
343
+ // Repeated at the top level, not only inside serverInfo. Registries look
344
+ // for these where their own schema puts them, and a scanner that finds
345
+ // nothing publishes a listing with a blank description and a placeholder
346
+ // icon — which is exactly what happened to apiosk/apiosk on first
347
+ // publish. Duplication is cheap; an empty storefront is not.
348
+ name: SERVER_INFO.name,
349
+ title: SERVER_INFO.title,
350
+ description: SERVER_DESCRIPTION,
351
+ version: SERVER_INFO.version,
352
+ websiteUrl: "https://apiosk.com",
353
+ icons: SERVER_INFO.icons,
354
+ instructions: SERVER_INSTRUCTIONS,
355
+ authentication: {
356
+ required: true,
357
+ schemes: ["oauth2"],
358
+ },
359
+ capabilities: {
360
+ tools: {},
361
+ resources: {},
362
+ prompts: {},
363
+ },
364
+ tools,
365
+ prompts: [],
366
+ resources: [V2_RESOURCE],
367
+ });
368
+ } catch (error) {
369
+ res.status(500).json({
370
+ error: "server_card_unavailable",
371
+ message: error instanceof Error ? error.message : String(error),
372
+ });
373
+ }
374
+ });
375
+
376
+ // Machine-readable index of every paid x402 route published through Apiosk,
377
+ // reshaped from the gateway's /.well-known/x402 document (60s cache).
378
+ app.get(["/.well-known/apiosk-routes.json", "/discovery"], async (req, res) => {
379
+ try {
380
+ const document = await buildDiscoveryDocument({ env: process.env });
381
+ res.setHeader("cache-control", "public, max-age=60");
382
+ res.json(document);
383
+ } catch (error) {
384
+ res.status(502).json({
385
+ error: "bad_gateway",
386
+ message: error instanceof Error ? error.message : String(error),
387
+ status: 502,
388
+ });
389
+ }
390
+ });
391
+
273
392
  app.get("/health", async (req, res) => {
274
393
  try {
275
394
  const tools = await listApioskTools({ runtime });
@@ -288,7 +407,7 @@ app.get("/health", async (req, res) => {
288
407
  });
289
408
 
290
409
  app.post("/mcp", mcpAuthMiddleware, async (req, res) => {
291
- const server = createApioskMcpServer({ runtime });
410
+ const server = createApioskMcpServer({ runtime, legacyUiMime: /openai|chatgpt/i.test(req.headers["user-agent"] || "") });
292
411
  const transport = new StreamableHTTPServerTransport({
293
412
  sessionIdGenerator: undefined,
294
413
  });
@@ -354,10 +473,78 @@ app.delete("/mcp", (req, res) => {
354
473
  });
355
474
  });
356
475
 
357
- app.listen(port, "0.0.0.0", async () => {
358
- console.log(`Apiosk MCP server listening on http://0.0.0.0:${port}`);
359
- console.log(`Health check: http://0.0.0.0:${port}/health`);
360
- console.log(`MCP endpoint: http://0.0.0.0:${port}/mcp`);
476
+ // Legacy HTTP+SSE transport (protocol version 2024-11-05), kept alongside
477
+ // Streamable HTTP for older MCP clients. Current ChatGPT and Claude installs
478
+ // should use the preferred /mcp Streamable HTTP endpoint.
479
+ const sseTransports = new Map();
480
+
481
+ app.get("/sse", async (req, res) => {
482
+ const server = createApioskMcpServer({ runtime });
483
+ const transport = new SSEServerTransport("/messages", res);
484
+ sseTransports.set(transport.sessionId, transport);
485
+
486
+ // Observability: record the SSE connection (the "who's connected / installs"
487
+ // roster) and mark it closed on disconnect. Fire-and-forget.
488
+ openSession(process.env, {
489
+ sessionId: transport.sessionId,
490
+ transport: "sse",
491
+ ip: req.ip,
492
+ userAgent: req.headers["user-agent"],
493
+ });
494
+
495
+ res.on("close", () => {
496
+ sseTransports.delete(transport.sessionId);
497
+ closeSession(process.env, transport.sessionId);
498
+ transport.close().catch(() => {});
499
+ server.close().catch(() => {});
500
+ });
501
+
502
+ try {
503
+ await server.connect(transport);
504
+ } catch (error) {
505
+ console.error("Error establishing /sse stream:", error);
506
+ sseTransports.delete(transport.sessionId);
507
+ }
508
+ });
509
+
510
+ app.post("/messages", mcpAuthMiddleware, async (req, res) => {
511
+ const sessionId = typeof req.query.sessionId === "string" ? req.query.sessionId : "";
512
+ const transport = sseTransports.get(sessionId);
513
+
514
+ if (!transport) {
515
+ res.status(400).json({
516
+ jsonrpc: "2.0",
517
+ id: null,
518
+ error: {
519
+ code: -32000,
520
+ message: "Bad Request: No SSE session found for the given sessionId.",
521
+ },
522
+ });
523
+ return;
524
+ }
525
+
526
+ try {
527
+ await transport.handlePostMessage(req, res, req.body);
528
+ } catch (error) {
529
+ console.error("Error handling /messages request:", error);
530
+ if (!res.headersSent) {
531
+ res.status(500).json({
532
+ jsonrpc: "2.0",
533
+ id: null,
534
+ error: {
535
+ code: -32603,
536
+ message: error instanceof Error ? error.message : String(error),
537
+ },
538
+ });
539
+ }
540
+ }
541
+ });
542
+
543
+ app.listen(port, bindHost, async () => {
544
+ console.log(`Apiosk MCP server listening on http://${bindHost}:${port}`);
545
+ console.log(`Health check: http://${bindHost}:${port}/health`);
546
+ console.log(`MCP endpoint: http://${bindHost}:${port}/mcp`);
547
+ console.log(`Legacy SSE endpoint: http://${bindHost}:${port}/sse`);
361
548
  console.log(`OAuth issuer: ${issuerUrl.href}`);
362
549
  console.log(`OAuth protected-resource metadata: ${hostedOAuth.resourceMetadataUrl}`);
363
550
  try {
@@ -0,0 +1,21 @@
1
+ // Self-contained so the card can use the same account-currency formatter.
2
+ export function approvalFeedback(data, money, fallback) {
3
+ const error = data?.errors?.find(error => typeof error?.message === 'string');
4
+ const message = data?.message || error?.message || fallback;
5
+ const details = error?.details;
6
+ if (!details || details.currency !== 'USD') return message;
7
+ const required = money(details.required_atomic, 'USD', true);
8
+ const available = money(details.available_atomic, 'USD');
9
+ if (required == null || available == null) return message;
10
+ const end = ' Nothing was purchased by this approval attempt.';
11
+ switch (error.code) {
12
+ case 'approval_per_request_limit':
13
+ return 'This request needs up to ' + required + ', but your connection allows ' + available + ' per request. Set higher limits below, or ask a smaller question.' + end;
14
+ case 'approval_daily_limit':
15
+ return 'This request needs up to ' + required + ', but your connection has ' + available + ' left in its daily budget. Set a higher daily limit below, try later, or ask a smaller question.' + end;
16
+ case 'approval_balance_insufficient':
17
+ return 'This request needs up to ' + required + ', but your available Apiosk balance is ' + available + '. Add funds in Apiosk > Balance, or ask a smaller question.' + end;
18
+ default:
19
+ return message;
20
+ }
21
+ }
@@ -0,0 +1,28 @@
1
+ import { fileURLToPath } from "node:url";
2
+
3
+ export const BRAND_LINKS = `<link rel="icon" type="image/png" href="/brand/apiosk-a-20260921.png" sizes="1254x1254" media="(prefers-color-scheme: light)" />
4
+ <link rel="icon" href="/favicon.ico?v=20260921" sizes="16x16 32x32 48x48" media="(prefers-color-scheme: light)" />
5
+ <link rel="icon" href="/favicon-dark.ico?v=20260918" sizes="16x16 32x32 48x48" media="(prefers-color-scheme: dark)" />
6
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg?v=20260918" />
7
+ <link rel="icon" type="image/png" sizes="512x512" href="/brand/mark-light-20260918.png" media="(prefers-color-scheme: light)" />
8
+ <link rel="icon" type="image/png" sizes="512x512" href="/brand/mark-dark-20260918.png" media="(prefers-color-scheme: dark)" />
9
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=20260918" />`;
10
+
11
+ export function registerBrandRoutes(app) {
12
+ for (const [path, filename, type] of [
13
+ ["/favicon.ico", "favicon.ico", "image/x-icon"],
14
+ ["/favicon-dark.ico", "favicon-dark.ico", "image/x-icon"],
15
+ ["/favicon.svg", "mark-20260918.svg", "image/svg+xml"],
16
+ ["/favicon.png", "apiosk-a-20260921.png", "image/png"],
17
+ ["/apple-touch-icon.png", "apple-touch-icon.png", "image/png"],
18
+ // Cached clients can still request earlier metadata and card URLs.
19
+ ["/brand/mark-20260905-transparent.svg", "mark-20260918.svg", "image/svg+xml"],
20
+ ["/brand/mark-light-20260905-transparent.png", "mark-light-20260918.png", "image/png"],
21
+ ["/brand/mark-dark-20260905-transparent.png", "mark-dark-20260918.png", "image/png"],
22
+ ]) {
23
+ app.get(path, (_req, res) => {
24
+ res.setHeader("cache-control", "public, max-age=0, must-revalidate");
25
+ res.type(type).sendFile(fileURLToPath(new URL(`../assets/brand/${filename}`, import.meta.url)));
26
+ });
27
+ }
28
+ }