@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/README.md +85 -9
- package/docs/sepa-rail.md +13 -13
- package/dxt.json +4 -4
- package/logo-optimized-light.png +0 -0
- package/package.json +6 -4
- package/server.json +58 -3
- package/server.mjs +213 -11
- package/src/assets/wallet-accounts.mjs +20513 -0
- package/src/assets/walletconnect-provider.mjs +6319 -0
- package/src/create-server.mjs +47 -2
- package/src/discovery.mjs +929 -0
- package/src/external-fetch.mjs +203 -0
- package/src/hosted-payment.mjs +552 -0
- package/src/hosted-wallets.mjs +530 -0
- package/src/oauth.mjs +2011 -225
- package/src/observability.mjs +194 -0
- package/src/payment-guidance.mjs +15 -24
- package/src/publisher.mjs +1288 -0
- package/src/result-canvas.mjs +16 -0
- package/src/runtime.mjs +609 -30
- package/src/settlement-disclosure.mjs +26 -0
- package/src/source-registry.mjs +215 -0
- package/src/x402-inspect.mjs +361 -0
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 {
|
|
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
|
|
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
|
|
171
|
+
<h1>Welcome to Apiosk Connect</h1>
|
|
156
172
|
<p class="lead">
|
|
157
173
|
This is the Apiosk MCP server endpoint — 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 & 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> — connect with
|
|
186
|
+
<code>Authorization: Bearer sk_live_…</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 & source</a>
|
|
181
206
|
</div>
|
|
182
|
-
<footer>Apiosk
|
|
207
|
+
<footer>Apiosk Connect · ${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
|
|
227
|
+
name: "Apiosk Connect",
|
|
203
228
|
server: SERVER_INFO,
|
|
204
229
|
description:
|
|
205
|
-
"Apiosk
|
|
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
|
-
|
|
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
|
|
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 {
|