@apiosk/mcp 1.3.1 → 1.7.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.
package/server.mjs CHANGED
@@ -1,16 +1,31 @@
1
1
  import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
2
+ import { fileURLToPath } from "node:url";
2
3
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
3
5
  import {
4
6
  SERVER_INFO,
5
7
  createApioskMcpServer,
6
8
  listApioskTools,
7
9
  } from "./src/create-server.mjs";
8
- import { createHostedOAuthSupport, resolveHostedMcpUrls } from "./src/oauth.mjs";
10
+ import {
11
+ createHostedOAuthSupport,
12
+ createMcpWalletAuthNonce,
13
+ resolveHostedMcpUrls,
14
+ } from "./src/oauth.mjs";
9
15
  import { createApioskMcpRuntime } from "./src/runtime.mjs";
16
+ import { openSession, closeSession } from "./src/observability.mjs";
10
17
  import {
11
18
  resolveOpenAiAppsChallengeToken,
12
19
  sendOpenAiAppsChallenge,
13
20
  } from "./well-known.mjs";
21
+ import {
22
+ buildDiscoveryDocument,
23
+ getOpenApiRouteDocument,
24
+ } from "./src/publisher.mjs";
25
+ import {
26
+ SETTLEMENT_DISCLOSURE_PATH,
27
+ createSettlementDisclosurePage,
28
+ } from "./src/settlement-disclosure.mjs";
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 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.
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,7 +127,7 @@ 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>
115
131
  <style>
116
132
  :root { color-scheme: dark; }
117
133
  * { box-sizing: border-box; }
@@ -152,7 +168,7 @@ function renderMcpWelcomeHtml(mcpUrl) {
152
168
  <main>
153
169
  <div class="card">
154
170
  <span class="badge">Model Context Protocol</span>
155
- <h1>Welcome to Apiosk MCP</h1>
171
+ <h1>Welcome to Apiosk Connect</h1>
156
172
  <p class="lead">
157
173
  This is the Apiosk MCP server endpoint &mdash; it lets AI agents discover, pay for,
158
174
  execute, and publish APIs through the Apiosk gateway. It is a machine endpoint, not a
@@ -166,6 +182,9 @@ function renderMcpWelcomeHtml(mcpUrl) {
166
182
  <li><strong>Pay</strong> per call automatically in USDC (x402 on Base).</li>
167
183
  <li><strong>Execute</strong> any listing through a uniform contract or its API-specific tool.</li>
168
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>
169
188
  </ul>
170
189
 
171
190
  <h2>Endpoint</h2>
@@ -174,12 +193,18 @@ function renderMcpWelcomeHtml(mcpUrl) {
174
193
  <h2>Connect from Claude Code</h2>
175
194
  <code>claude mcp add --transport http apiosk ${mcpUrl}</code>
176
195
 
196
+ <h2>Connect from ChatGPT</h2>
197
+ <code class="endpoint">${mcpUrl}</code>
198
+
199
+ <h2>Legacy SSE fallback</h2>
200
+ <code class="endpoint">${mcpUrl.replace(/\/mcp$/, "/sse")}</code>
201
+
177
202
  <div class="links">
178
203
  <a href="/health">Health</a>
179
204
  <a href="https://dashboard.apiosk.com" target="_blank" rel="noopener">Dashboard</a>
180
205
  <a href="https://github.com/obcraft/apiosk-mcp" target="_blank" rel="noopener">Docs &amp; source</a>
181
206
  </div>
182
- <footer>Apiosk MCP &middot; ${SERVER_INFO.name} v${SERVER_INFO.version}</footer>
207
+ <footer>Apiosk Connect &middot; ${SERVER_INFO.name} v${SERVER_INFO.version}</footer>
183
208
  </div>
184
209
  </main>
185
210
  </body>
@@ -199,12 +224,13 @@ function sendMcpWelcome(req, res) {
199
224
  }
200
225
 
201
226
  res.status(200).json({
202
- name: "Apiosk MCP",
227
+ name: "Apiosk Connect",
203
228
  server: SERVER_INFO,
204
229
  description:
205
- "Apiosk MCP server endpoint. Connect it from an MCP client to discover, pay for, execute, and publish APIs through the Apiosk gateway.",
230
+ "Apiosk Connect lets MCP clients discover, pay for, execute, and publish APIs through the Apiosk gateway.",
206
231
  transport: "streamable-http",
207
232
  endpoint: mcpUrl,
233
+ legacy_sse_endpoint: mcpUrl.replace(/\/mcp$/, "/sse"),
208
234
  connect: {
209
235
  claude_code: `claude mcp add --transport http apiosk ${mcpUrl}`,
210
236
  },
@@ -216,22 +242,27 @@ function sendMcpWelcome(req, res) {
216
242
  // Public Fly deployment must accept the Fly hostname instead of localhost-only
217
243
  // host validation defaults.
218
244
  const app = createMcpExpressApp({ host: "0.0.0.0" });
245
+ // Fly terminates TLS and adds one trusted proxy hop. Without this,
246
+ // express-rate-limit rejects X-Forwarded-For and can surface as MCP -32603.
247
+ app.set("trust proxy", 1);
219
248
  const port = Number(process.env.PORT || 3000);
220
249
  const { issuerUrl, mcpServerUrl } = resolveHostedMcpUrls({
221
250
  env: process.env,
222
251
  port,
223
252
  });
224
253
  const runtime = createApioskMcpRuntime({
225
- enableLocalWallets: process.env.APIOSK_ENABLE_LOCAL_WALLETS === "true",
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,
226
258
  hostedAuthEnabled: true,
227
259
  });
228
260
  const hostedOAuth = createHostedOAuthSupport({
229
261
  env: process.env,
230
- controlPlaneBaseUrl: CONTROL_PLANE_BACKEND_URL,
231
262
  issuerUrl,
232
263
  mcpServerUrl,
233
264
  appName: "Apiosk",
234
- resourceName: "Apiosk MCP",
265
+ resourceName: "Apiosk Connect",
235
266
  });
236
267
  const mcpAuthMiddleware = hostedOAuth.createMcpAuthMiddleware(runtime);
237
268
 
@@ -249,6 +280,66 @@ app.get(OPENAI_APPS_CHALLENGE_PATH_PATTERN, (req, res) => {
249
280
  return sendOpenAiAppsChallenge(res, OPENAI_APPS_CHALLENGE_TOKEN);
250
281
  });
251
282
 
283
+ app.get("/logo-optimized-light.png", (req, res) => {
284
+ res.type("png").sendFile(fileURLToPath(new URL("./logo-optimized-light.png", import.meta.url)));
285
+ });
286
+
287
+ app.get(SETTLEMENT_DISCLOSURE_PATH, (_req, res) => {
288
+ res
289
+ .setHeader("cache-control", "public, max-age=300")
290
+ .type("html")
291
+ .send(createSettlementDisclosurePage());
292
+ });
293
+
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
+
252
343
  app.all("/api/*path", async (req, res) => {
253
344
  try {
254
345
  const pathname = req.path || req.originalUrl || "";
@@ -270,6 +361,49 @@ app.all("/api/*path", async (req, res) => {
270
361
  }
271
362
  });
272
363
 
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) => {
369
+ try {
370
+ const document = await getOpenApiRouteDocument(req.params.routeId, {
371
+ env: process.env,
372
+ });
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
+ } catch (error) {
383
+ res.status(502).json({
384
+ error: "bad_gateway",
385
+ message: error instanceof Error ? error.message : String(error),
386
+ status: 502,
387
+ });
388
+ }
389
+ });
390
+
391
+ // Machine-readable index of every paid x402 route published through Apiosk,
392
+ // reshaped from the gateway's /.well-known/x402 document (60s cache).
393
+ app.get(["/.well-known/apiosk-routes.json", "/discovery"], async (req, res) => {
394
+ try {
395
+ const document = await buildDiscoveryDocument({ env: process.env });
396
+ res.setHeader("cache-control", "public, max-age=60");
397
+ res.json(document);
398
+ } catch (error) {
399
+ res.status(502).json({
400
+ error: "bad_gateway",
401
+ message: error instanceof Error ? error.message : String(error),
402
+ status: 502,
403
+ });
404
+ }
405
+ });
406
+
273
407
  app.get("/health", async (req, res) => {
274
408
  try {
275
409
  const tools = await listApioskTools({ runtime });
@@ -354,10 +488,78 @@ app.delete("/mcp", (req, res) => {
354
488
  });
355
489
  });
356
490
 
491
+ // Legacy HTTP+SSE transport (protocol version 2024-11-05), kept alongside
492
+ // Streamable HTTP for older MCP clients. Current ChatGPT and Claude installs
493
+ // should use the preferred /mcp Streamable HTTP endpoint.
494
+ const sseTransports = new Map();
495
+
496
+ app.get("/sse", async (req, res) => {
497
+ const server = createApioskMcpServer({ runtime });
498
+ const transport = new SSEServerTransport("/messages", res);
499
+ sseTransports.set(transport.sessionId, transport);
500
+
501
+ // Observability: record the SSE connection (the "who's connected / installs"
502
+ // roster) and mark it closed on disconnect. Fire-and-forget.
503
+ openSession(process.env, {
504
+ sessionId: transport.sessionId,
505
+ transport: "sse",
506
+ ip: req.ip,
507
+ userAgent: req.headers["user-agent"],
508
+ });
509
+
510
+ res.on("close", () => {
511
+ sseTransports.delete(transport.sessionId);
512
+ closeSession(process.env, transport.sessionId);
513
+ transport.close().catch(() => {});
514
+ server.close().catch(() => {});
515
+ });
516
+
517
+ try {
518
+ await server.connect(transport);
519
+ } catch (error) {
520
+ console.error("Error establishing /sse stream:", error);
521
+ sseTransports.delete(transport.sessionId);
522
+ }
523
+ });
524
+
525
+ app.post("/messages", mcpAuthMiddleware, async (req, res) => {
526
+ const sessionId = typeof req.query.sessionId === "string" ? req.query.sessionId : "";
527
+ const transport = sseTransports.get(sessionId);
528
+
529
+ if (!transport) {
530
+ res.status(400).json({
531
+ jsonrpc: "2.0",
532
+ id: null,
533
+ error: {
534
+ code: -32000,
535
+ message: "Bad Request: No SSE session found for the given sessionId.",
536
+ },
537
+ });
538
+ return;
539
+ }
540
+
541
+ try {
542
+ await transport.handlePostMessage(req, res, req.body);
543
+ } catch (error) {
544
+ console.error("Error handling /messages request:", error);
545
+ if (!res.headersSent) {
546
+ res.status(500).json({
547
+ jsonrpc: "2.0",
548
+ id: null,
549
+ error: {
550
+ code: -32603,
551
+ message: error instanceof Error ? error.message : String(error),
552
+ },
553
+ });
554
+ }
555
+ }
556
+ });
557
+
357
558
  app.listen(port, "0.0.0.0", async () => {
358
559
  console.log(`Apiosk MCP server listening on http://0.0.0.0:${port}`);
359
560
  console.log(`Health check: http://0.0.0.0:${port}/health`);
360
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`);
361
563
  console.log(`OAuth issuer: ${issuerUrl.href}`);
362
564
  console.log(`OAuth protected-resource metadata: ${hostedOAuth.resourceMetadataUrl}`);
363
565
  try {