@apiosk/mcp 1.7.0 → 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 (100) hide show
  1. package/README.md +83 -612
  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/dxt.json +26 -22
  30. package/index.mjs +3 -1
  31. package/logo-optimized-light.png +0 -0
  32. package/package.json +14 -14
  33. package/plugin/apiosk/.codex-plugin/plugin.json +45 -0
  34. package/plugin/apiosk/.mcp.json +8 -0
  35. package/plugin/apiosk/assets/icon-dark.png +0 -0
  36. package/plugin/apiosk/assets/icon.png +0 -0
  37. package/plugin/apiosk/assets/icon.svg +1 -0
  38. package/plugin/apiosk/assets/logo.png +0 -0
  39. package/plugin/apiosk/skills/apiosk/SKILL.md +33 -0
  40. package/plugin/apiosk/skills/apiosk/agents/openai.yaml +12 -0
  41. package/server.json +58 -24
  42. package/server.mjs +136 -151
  43. package/src/approval-feedback.mjs +21 -0
  44. package/src/brand-routes.mjs +28 -0
  45. package/src/create-server.mjs +160 -46
  46. package/src/display-money.mjs +20 -0
  47. package/src/display-text.mjs +67 -0
  48. package/src/gateway-client.mjs +42 -0
  49. package/src/gateway-v2-ask.mjs +50 -0
  50. package/src/gateway-v2-card-account.mjs +23 -0
  51. package/src/gateway-v2-card-actions.mjs +67 -0
  52. package/src/gateway-v2-card-answer-text.mjs +130 -0
  53. package/src/gateway-v2-card-answer.mjs +68 -0
  54. package/src/gateway-v2-card-blocks.mjs +166 -0
  55. package/src/gateway-v2-card-body.mjs +259 -0
  56. package/src/gateway-v2-card-budget.mjs +21 -0
  57. package/src/gateway-v2-card-cbs.mjs +56 -0
  58. package/src/gateway-v2-card-choices.mjs +4 -0
  59. package/src/gateway-v2-card-clarification.mjs +25 -0
  60. package/src/gateway-v2-card-compact.mjs +34 -0
  61. package/src/gateway-v2-card-events.mjs +41 -0
  62. package/src/gateway-v2-card-presentation.mjs +125 -0
  63. package/src/gateway-v2-card-research.mjs +63 -0
  64. package/src/gateway-v2-card-result.mjs +32 -0
  65. package/src/gateway-v2-card-search.mjs +39 -0
  66. package/src/gateway-v2-card-sources.mjs +32 -0
  67. package/src/gateway-v2-card-style.mjs +76 -0
  68. package/src/gateway-v2-card-verdict.mjs +66 -0
  69. package/src/gateway-v2-card.mjs +91 -0
  70. package/src/gateway-v2-contracts.json +66 -0
  71. package/src/gateway-v2-instructions.md +113 -0
  72. package/src/gateway-v2-recovery.mjs +18 -0
  73. package/src/gateway-v2-report-links.mjs +14 -0
  74. package/src/gateway-v2-workflows.mjs +17 -0
  75. package/src/gateway-v2.mjs +179 -0
  76. package/src/oauth.mjs +402 -1949
  77. package/src/observability.mjs +19 -3
  78. package/src/result-presentation.mjs +9 -0
  79. package/src/runtime.mjs +24 -3670
  80. package/src/source-groups.mjs +91 -0
  81. package/src/source-value-format.mjs +25 -0
  82. package/src/tool-result.mjs +20 -0
  83. package/src/ui-bridge.mjs +185 -0
  84. package/src/well-known-routes.mjs +132 -0
  85. package/src/assets/wallet-accounts.mjs +0 -20513
  86. package/src/assets/walletconnect-provider.mjs +0 -6319
  87. package/src/discovery.mjs +0 -929
  88. package/src/external-fetch.mjs +0 -203
  89. package/src/funding-options.mjs +0 -255
  90. package/src/gateway-management.mjs +0 -107
  91. package/src/hosted-payment.mjs +0 -552
  92. package/src/hosted-wallets.mjs +0 -530
  93. package/src/listing-metadata.mjs +0 -287
  94. package/src/local-config.mjs +0 -256
  95. package/src/payment-guidance.mjs +0 -298
  96. package/src/publisher.mjs +0 -1288
  97. package/src/result-canvas.mjs +0 -16
  98. package/src/source-registry.mjs +0 -215
  99. package/src/wallet-store.mjs +0 -476
  100. package/src/x402-inspect.mjs +0 -361
package/server.json CHANGED
@@ -2,24 +2,40 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.obcraft/apiosk-mcp",
4
4
  "title": "Apiosk Connect",
5
- "description": "Discover, pay for, execute, and publish APIs through Apiosk.",
6
- "version": "1.7.0",
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://mcp.apiosk.com/logo-optimized-light.png",
10
+ "src": "https://mcp.apiosk.com/brand/apiosk-a-20260921.png",
11
11
  "mimeType": "image/png",
12
- "sizes": ["2048x2048"]
12
+ "sizes": [
13
+ "1254x1254"
14
+ ],
15
+ "theme": "light"
13
16
  },
14
17
  {
15
- "src": "https://apiosk.com/logo.svg",
18
+ "src": "https://mcp.apiosk.com/brand/mark-20260918.svg",
16
19
  "mimeType": "image/svg+xml",
17
- "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"
18
31
  },
19
32
  {
20
- "src": "https://apiosk.com/apple-touch-icon.png",
33
+ "src": "https://mcp.apiosk.com/brand/mark-dark-20260918.png",
21
34
  "mimeType": "image/png",
22
- "sizes": ["180x180"]
35
+ "sizes": [
36
+ "512x512"
37
+ ],
38
+ "theme": "dark"
23
39
  }
24
40
  ],
25
41
  "repository": {
@@ -30,23 +46,23 @@
30
46
  {
31
47
  "registryType": "npm",
32
48
  "identifier": "@apiosk/mcp",
33
- "version": "1.7.0",
49
+ "version": "2.0.0",
34
50
  "transport": {
35
51
  "type": "stdio"
36
52
  },
37
53
  "environmentVariables": [
38
54
  {
39
- "name": "APIOSK_PRIVATE_KEY",
40
- "description": "Optional wallet private key for automatic x402 payments.",
41
- "isRequired": false,
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,
42
58
  "isSecret": true,
43
59
  "format": "string"
44
60
  },
45
61
  {
46
- "name": "APIOSK_CONNECT_TOKEN",
47
- "description": "Optional dashboard-managed access token.",
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.",
48
64
  "isRequired": false,
49
- "isSecret": true,
65
+ "isSecret": false,
50
66
  "format": "string"
51
67
  }
52
68
  ]
@@ -54,23 +70,23 @@
54
70
  {
55
71
  "registryType": "pypi",
56
72
  "identifier": "apiosk-mcp",
57
- "version": "1.7.0",
73
+ "version": "2.0.0",
58
74
  "transport": {
59
75
  "type": "stdio"
60
76
  },
61
77
  "environmentVariables": [
62
78
  {
63
- "name": "APIOSK_PRIVATE_KEY",
64
- "description": "Optional wallet private key for automatic x402 payments.",
65
- "isRequired": false,
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,
66
82
  "isSecret": true,
67
83
  "format": "string"
68
84
  },
69
85
  {
70
- "name": "APIOSK_CONNECT_TOKEN",
71
- "description": "Optional dashboard-managed access token.",
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.",
72
88
  "isRequired": false,
73
- "isSecret": true,
89
+ "isSecret": false,
74
90
  "format": "string"
75
91
  }
76
92
  ]
@@ -84,8 +100,26 @@
84
100
  ],
85
101
  "_meta": {
86
102
  "com.apiosk": {
87
- "tags": ["payments", "finance", "x402", "commerce", "crypto"],
88
- "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
+ ]
89
123
  }
90
124
  }
91
125
  }
package/server.mjs CHANGED
@@ -1,15 +1,16 @@
1
+ import { registerBrandRoutes, BRAND_LINKS } from "./src/brand-routes.mjs";
1
2
  import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
5
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
5
6
  import {
6
- SERVER_INFO,
7
+ resolveServerPresentation,
7
8
  createApioskMcpServer,
8
9
  listApioskTools,
9
10
  } from "./src/create-server.mjs";
11
+ import { V2_RESOURCE } from "./src/gateway-v2.mjs";
10
12
  import {
11
13
  createHostedOAuthSupport,
12
- createMcpWalletAuthNonce,
13
14
  resolveHostedMcpUrls,
14
15
  } from "./src/oauth.mjs";
15
16
  import { createApioskMcpRuntime } from "./src/runtime.mjs";
@@ -18,15 +19,14 @@ import {
18
19
  resolveOpenAiAppsChallengeToken,
19
20
  sendOpenAiAppsChallenge,
20
21
  } from "./well-known.mjs";
21
- import {
22
- buildDiscoveryDocument,
23
- getOpenApiRouteDocument,
24
- } from "./src/publisher.mjs";
22
+ import { buildDiscoveryDocument } from "./src/well-known-routes.mjs";
25
23
  import {
26
24
  SETTLEMENT_DISCLOSURE_PATH,
27
25
  createSettlementDisclosurePage,
28
26
  } from "./src/settlement-disclosure.mjs";
29
27
 
28
+ const { info: SERVER_INFO, description: SERVER_DESCRIPTION, instructions: SERVER_INSTRUCTIONS } = resolveServerPresentation();
29
+
30
30
  const CONTROL_PLANE_BACKEND_URL = (
31
31
  process.env.APIOSK_CONTROL_PLANE_BACKEND_URL ||
32
32
  process.env.APIOSK_DASHBOARD_URL ||
@@ -61,9 +61,9 @@ function normalizeControlPlanePath(pathname = "") {
61
61
  }
62
62
 
63
63
  function shouldProxyControlPlanePath(pathname = "") {
64
- // Hosted sign-in is wallet-only now (see src/oauth.mjs); the email/password
65
- // /api/auth/mcp-sign-in|sign-up routes never existed on the control plane, so
66
- // they are no longer proxied.
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.
67
67
  const normalizedPath = normalizeControlPlanePath(pathname);
68
68
  return (
69
69
  normalizedPath.startsWith("/api/credits/") ||
@@ -128,76 +128,33 @@ function renderMcpWelcomeHtml(mcpUrl) {
128
128
  <meta name="viewport" content="width=device-width, initial-scale=1" />
129
129
  <meta name="robots" content="noindex" />
130
130
  <title>Apiosk Connect</title>
131
+ ${BRAND_LINKS}
131
132
  <style>
132
- :root { color-scheme: dark; }
133
- * { box-sizing: border-box; }
134
- body {
135
- margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
136
- background: radial-gradient(1200px 600px at 50% -10%, #1b2230, #0b0e14 60%);
137
- color: #e6e9ef; font: 15px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
138
- padding: 32px;
139
- }
140
- main { max-width: 640px; width: 100%; }
141
- .badge {
142
- display: inline-block; font-size: 12px; letter-spacing: .08em; text-transform: uppercase;
143
- color: #8aa0c6; border: 1px solid #2a3344; border-radius: 999px; padding: 4px 12px; margin-bottom: 20px;
144
- }
145
- h1 { font-size: 30px; margin: 0 0 12px; letter-spacing: -.02em; }
146
- p { color: #b8c0cf; margin: 0 0 16px; }
147
- .lead { color: #d6dce6; font-size: 17px; }
148
- ul { color: #b8c0cf; margin: 0 0 16px; padding-left: 20px; }
149
- li { margin: 6px 0; }
150
- code {
151
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;
152
- background: #141a24; border: 1px solid #232c3a; border-radius: 6px; padding: 2px 7px; color: #cfe0ff;
153
- }
154
- .card { background: #0f141c; border: 1px solid #1f2937; border-radius: 14px; padding: 28px 30px; }
155
- .endpoint {
156
- display: block; margin: 4px 0 20px; padding: 12px 14px; background: #141a24;
157
- border: 1px solid #232c3a; border-radius: 8px; color: #cfe0ff; word-break: break-all;
158
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;
159
- }
160
- h2 { font-size: 13px; letter-spacing: .06em; text-transform: uppercase; color: #7f8aa0; margin: 24px 0 8px; }
161
- a { color: #7aa2ff; text-decoration: none; }
162
- a:hover { text-decoration: underline; }
163
- .links { display: flex; gap: 18px; flex-wrap: wrap; margin-top: 18px; padding-top: 18px; border-top: 1px solid #1f2937; }
164
- 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}
165
144
  </style>
166
145
  </head>
167
146
  <body>
168
147
  <main>
169
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>
170
150
  <span class="badge">Model Context Protocol</span>
171
- <h1>Welcome to Apiosk Connect</h1>
172
- <p class="lead">
173
- This is the Apiosk MCP server endpoint &mdash; it lets AI agents discover, pay for,
174
- execute, and publish APIs through the Apiosk gateway. It is a machine endpoint, not a
175
- website, so connect it from an MCP client (Claude, Cursor, ChatGPT, and others) rather
176
- than browsing it here.
177
- </p>
178
-
179
- <h2>What you can do with it</h2>
180
- <ul>
181
- <li><strong>Discover</strong> APIs, datasets, and services in the Apiosk catalog.</li>
182
- <li><strong>Pay</strong> per call automatically in USDC (x402 on Base).</li>
183
- <li><strong>Execute</strong> any listing through a uniform contract or its API-specific tool.</li>
184
- <li><strong>Publish &amp; manage</strong> your own APIs so other agents can find and pay for them.</li>
185
- <li><strong>Ship paid x402 routes from a coding agent</strong> &mdash; connect with
186
- <code>Authorization: Bearer sk_live_&hellip;</code> (a provider API key) and use
187
- <code>publish_x402_route</code>, <code>test_x402_route</code>, and friends.</li>
188
- </ul>
189
-
190
- <h2>Endpoint</h2>
191
- <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>
192
154
 
193
- <h2>Connect from Claude Code</h2>
194
- <code>claude mcp add --transport http apiosk ${mcpUrl}</code>
195
-
196
- <h2>Connect from ChatGPT</h2>
155
+ <h2>Secure MCP endpoint</h2>
197
156
  <code class="endpoint">${mcpUrl}</code>
198
-
199
- <h2>Legacy SSE fallback</h2>
200
- <code class="endpoint">${mcpUrl.replace(/\/mcp$/, "/sse")}</code>
157
+ <details><summary>Manual setup</summary><code>claude mcp add --transport http apiosk ${mcpUrl}</code><code>${mcpUrl.replace(/\/mcp$/, "/sse")}</code></details>
201
158
 
202
159
  <div class="links">
203
160
  <a href="/health">Health</a>
@@ -226,8 +183,7 @@ function sendMcpWelcome(req, res) {
226
183
  res.status(200).json({
227
184
  name: "Apiosk Connect",
228
185
  server: SERVER_INFO,
229
- description:
230
- "Apiosk Connect lets MCP clients discover, pay for, execute, and publish APIs through the Apiosk gateway.",
186
+ description: SERVER_DESCRIPTION,
231
187
  transport: "streamable-http",
232
188
  endpoint: mcpUrl,
233
189
  legacy_sse_endpoint: mcpUrl.replace(/\/mcp$/, "/sse"),
@@ -241,7 +197,8 @@ function sendMcpWelcome(req, res) {
241
197
 
242
198
  // Public Fly deployment must accept the Fly hostname instead of localhost-only
243
199
  // host validation defaults.
244
- 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 });
245
202
  // Fly terminates TLS and adds one trusted proxy hop. Without this,
246
203
  // express-rate-limit rejects X-Forwarded-For and can surface as MCP -32603.
247
204
  app.set("trust proxy", 1);
@@ -251,10 +208,8 @@ const { issuerUrl, mcpServerUrl } = resolveHostedMcpUrls({
251
208
  port,
252
209
  });
253
210
  const runtime = createApioskMcpRuntime({
254
- // Hosted MCP is multi-tenant and must never read/write machine-local wallet
255
- // or config state. Buyer identity and payment capability are request-scoped
256
- // through OAuth/connect tokens; local wallets belong only to stdio installs.
257
- enableLocalWallets: false,
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.
258
213
  hostedAuthEnabled: true,
259
214
  });
260
215
  const hostedOAuth = createHostedOAuthSupport({
@@ -266,6 +221,24 @@ const hostedOAuth = createHostedOAuthSupport({
266
221
  });
267
222
  const mcpAuthMiddleware = hostedOAuth.createMcpAuthMiddleware(runtime);
268
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
+
269
242
  app.use(hostedOAuth.metadataRouter);
270
243
  app.use(new URL(hostedOAuth.oauthMetadata.authorization_endpoint).pathname, hostedOAuth.authorizationRouter);
271
244
  app.use(new URL(hostedOAuth.oauthMetadata.token_endpoint).pathname, hostedOAuth.tokenRouter);
@@ -275,15 +248,55 @@ if (hostedOAuth.oauthMetadata.registration_endpoint) {
275
248
  hostedOAuth.registrationRouter
276
249
  );
277
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));
278
253
 
279
254
  app.get(OPENAI_APPS_CHALLENGE_PATH_PATTERN, (req, res) => {
280
255
  return sendOpenAiAppsChallenge(res, OPENAI_APPS_CHALLENGE_TOKEN);
281
256
  });
282
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.
283
267
  app.get("/logo-optimized-light.png", (req, res) => {
268
+ res.setHeader("cache-control", "public, max-age=0, must-revalidate");
284
269
  res.type("png").sendFile(fileURLToPath(new URL("./logo-optimized-light.png", import.meta.url)));
285
270
  });
286
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
+
287
300
  app.get(SETTLEMENT_DISCLOSURE_PATH, (_req, res) => {
288
301
  res
289
302
  .setHeader("cache-control", "public, max-age=300")
@@ -291,55 +304,6 @@ app.get(SETTLEMENT_DISCLOSURE_PATH, (_req, res) => {
291
304
  .send(createSettlementDisclosurePage());
292
305
  });
293
306
 
294
- // Self-hosted browser bundles for the /authorize wallet flows. Served
295
- // same-origin because embedded mobile browsers can refuse cross-origin dynamic
296
- // module imports. Regenerate with scripts/build-wallet-lib.mjs after upgrades.
297
- const browserBundleFiles = new Map([
298
- ["wallet-accounts.mjs", new URL("./src/assets/wallet-accounts.mjs", import.meta.url)],
299
- ["walletconnect-provider.mjs", new URL("./src/assets/walletconnect-provider.mjs", import.meta.url)],
300
- ]);
301
- const browserBundleCache = new Map();
302
-
303
- app.get("/assets/:bundle", async (req, res, next) => {
304
- const bundleUrl = browserBundleFiles.get(req.params.bundle);
305
- if (!bundleUrl) return next();
306
-
307
- try {
308
- let bundle = browserBundleCache.get(req.params.bundle);
309
- if (!bundle) {
310
- const { readFile } = await import("node:fs/promises");
311
- bundle = await readFile(bundleUrl);
312
- browserBundleCache.set(req.params.bundle, bundle);
313
- }
314
- res.setHeader("content-type", "text/javascript; charset=utf-8");
315
- res.setHeader("cache-control", "public, max-age=3600");
316
- res.send(bundle);
317
- } catch (error) {
318
- res.status(404).json({
319
- error: "not_found",
320
- message: "Wallet library bundle is not available on this deployment.",
321
- status: 404,
322
- });
323
- }
324
- });
325
-
326
- app.post("/api/auth/mcp-wallet-nonce", async (req, res) => {
327
- try {
328
- const nonce = await createMcpWalletAuthNonce({ env: process.env });
329
- res.status(200).json(nonce);
330
- } catch (error) {
331
- const status = Number.isFinite(error?.status) ? error.status : 502;
332
- res.status(status).json({
333
- error: "wallet_auth_unavailable",
334
- message:
335
- error instanceof Error
336
- ? error.message
337
- : "Wallet sign-in is temporarily unavailable.",
338
- status,
339
- });
340
- }
341
- });
342
-
343
307
  app.all("/api/*path", async (req, res) => {
344
308
  try {
345
309
  const pathname = req.path || req.originalUrl || "";
@@ -361,29 +325,50 @@ app.all("/api/*path", async (req, res) => {
361
325
  }
362
326
  });
363
327
 
364
- // Hosted OpenAPI spec for a published x402 route (generate_openapi_spec
365
- // returns this URL). Built live from the gateway database, so it always
366
- // reflects the current listing. Accepts /openapi/<route_id> and
367
- // /openapi/<route_id>.json.
368
- app.get("/openapi/:routeId", async (req, res) => {
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) => {
369
338
  try {
370
- const document = await getOpenApiRouteDocument(req.params.routeId, {
371
- env: process.env,
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],
372
367
  });
373
- if (!document) {
374
- return res.status(404).json({
375
- error: "not_found",
376
- message: "No published route matches this id.",
377
- status: 404,
378
- });
379
- }
380
- res.setHeader("cache-control", "public, max-age=60");
381
- res.json(document);
382
368
  } catch (error) {
383
- res.status(502).json({
384
- error: "bad_gateway",
369
+ res.status(500).json({
370
+ error: "server_card_unavailable",
385
371
  message: error instanceof Error ? error.message : String(error),
386
- status: 502,
387
372
  });
388
373
  }
389
374
  });
@@ -422,7 +407,7 @@ app.get("/health", async (req, res) => {
422
407
  });
423
408
 
424
409
  app.post("/mcp", mcpAuthMiddleware, async (req, res) => {
425
- const server = createApioskMcpServer({ runtime });
410
+ const server = createApioskMcpServer({ runtime, legacyUiMime: /openai|chatgpt/i.test(req.headers["user-agent"] || "") });
426
411
  const transport = new StreamableHTTPServerTransport({
427
412
  sessionIdGenerator: undefined,
428
413
  });
@@ -555,11 +540,11 @@ app.post("/messages", mcpAuthMiddleware, async (req, res) => {
555
540
  }
556
541
  });
557
542
 
558
- app.listen(port, "0.0.0.0", async () => {
559
- console.log(`Apiosk MCP server listening on http://0.0.0.0:${port}`);
560
- console.log(`Health check: http://0.0.0.0:${port}/health`);
561
- console.log(`MCP endpoint: http://0.0.0.0:${port}/mcp`);
562
- console.log(`Legacy SSE endpoint: http://0.0.0.0:${port}/sse`);
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`);
563
548
  console.log(`OAuth issuer: ${issuerUrl.href}`);
564
549
  console.log(`OAuth protected-resource metadata: ${hostedOAuth.resourceMetadataUrl}`);
565
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
+ }