@apiosk/mcp 1.3.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.
@@ -0,0 +1,134 @@
1
+ # Settlement rails — USDC, SEPA incasso, credits
2
+
3
+ Apiosk is **one mandate, any rail**. A single buyer connect token can settle a
4
+ paid API call over any of three rails. The gateway picks the rail per call —
5
+ the agent does not need to know or choose which one is used.
6
+
7
+ | Rail | What it is | Best for |
8
+ | --- | --- | --- |
9
+ | `usdc_x402` | On-chain USDC on Base (chain `8453`), settled per call via an x402 payment proof from the agent wallet or `APIOSK_PRIVATE_KEY`. | Crypto-native agents with a funded wallet. |
10
+ | `sepa_incasso` | EU **SEPA Direct Debit** (incasso). Paid calls are appended to a ledger and collected later in batches from the buyer's bank account. No per-call bank transaction. | EU buyers; high call volume; lowest fees. |
11
+ | `credits` | Prepaid balance topped up once by a human (Adyen/Mollie), then spent down per call (`apiosk_buy_credits`). | Humans who want to pre-fund and let an agent spend. |
12
+
13
+ ### Rail fallback order
14
+
15
+ 1. **USDC / x402 wallet** — when the agent can produce a payment proof.
16
+ 2. **SEPA incasso ledger** — when the buyer has an active SEPA mandate. No proof
17
+ needed: the call is *recorded*, not blocked.
18
+ 3. **Prepaid credits** balance.
19
+
20
+ A `402 Payment Required` is only returned when none of the buyer's enabled rails
21
+ can cover the call.
22
+
23
+ ---
24
+
25
+ ## SEPA incasso (direct debit), end to end
26
+
27
+ An incasso lets Apiosk pull euros from the buyer's bank account under a one-time
28
+ mandate, so an agent can keep calling paid APIs without signing or funding each
29
+ call.
30
+
31
+ ### 1. Mandate setup — once, by a human
32
+
33
+ In the buyer portal the buyer authorizes a recurring SEPA mandate:
34
+
35
+ - **iDEAL** (NL): a one-cent verification payment returns a reusable
36
+ direct-debit mandate. Recurring calls then use `method=directdebit`.
37
+ - **PayPal** / **card**: alternative mandates for non-NL buyers (recurring calls
38
+ use `method=paypal` / `method=creditcard`).
39
+
40
+ Agents never perform this step — they only need the connect token afterward.
41
+
42
+ ### 2. Collection terms — once, by the buyer
43
+
44
+ The mandate authorizes bounded collection:
45
+
46
+ - **Threshold:** €25–€500 (default €25, floor €25).
47
+ - **Max age:** 7, 14, or 30 days.
48
+
49
+ These bound how much, and for how long, calls can accrue before a collection is
50
+ triggered.
51
+
52
+ ### 3. Per call — automatic, deferred
53
+
54
+ When a paid call settles over SEPA, the gateway appends a **SEPA ledger debit**
55
+ row carrying the full breakdown and returns success immediately:
56
+
57
+ ```json
58
+ {
59
+ "total_eur": "0.10",
60
+ "apiosk_fee_rate": "0.03",
61
+ "apiosk_fee_pct": "3.0000",
62
+ "apiosk_fee_eur": "0.003",
63
+ "provider_net_eur": "0.097"
64
+ }
65
+ ```
66
+
67
+ No bank transaction happens yet — only a ledger entry.
68
+
69
+ ### 4. Batch collection — background worker
70
+
71
+ A worker flushes a buyer's unbatched ledger into **one** Mollie SEPA Direct
72
+ Debit when either condition is met:
73
+
74
+ - outstanding (unbatched) sum **crosses the threshold**, OR
75
+ - the **oldest unbatched entry passes the max age**.
76
+
77
+ Many micro-calls collapse into a single bank debit. The worker opens a batch via
78
+ the `sepa_open_settlement_batch` RPC and calls the `mollie_settle_sepa_batch`
79
+ edge function to create the direct debit.
80
+
81
+ ---
82
+
83
+ ## Economics
84
+
85
+ - **Apiosk platform fee: 2% by default** of each call's gross, recorded per ledger row.
86
+ - **Mollie SEPA Direct Debit fee: ~€0.30 per collection (per batch)** — not per
87
+ call. This is why sub-€25 thresholds are not offered: batching amortizes the
88
+ fixed bank fee across many calls.
89
+ - The provider receives gross minus the Apiosk platform fee; the Mollie fee is netted
90
+ at collection time.
91
+
92
+ ## Operational defaults
93
+
94
+ | Setting | Default | Env / source |
95
+ | --- | --- | --- |
96
+ | Buyer threshold | €25 (range 25–500) | buyer portal, `sepa_collection_threshold_eur` |
97
+ | Buyer max age | 7 / 14 / 30 days | buyer portal |
98
+ | Batch worker cadence | 1800 s | `SEPA_BATCH_POLL_SECONDS` |
99
+ | Batch flush amount | €25 | `MOLLIE_BATCH_FLUSH_EUR` |
100
+ | Batch flush age | 7 days | `MOLLIE_BATCH_FLUSH_DAYS` |
101
+
102
+ ## Connect string and SEPA
103
+
104
+ The connect string identifies the buyer's managed wallet and connect token:
105
+
106
+ ```bash
107
+ export APIO_GATEWAY_URL=https://gateway.apiosk.com
108
+ export APIO_CHAIN_ID=8453
109
+ export APIO_AGENT_WALLET_ADDRESS=0x...
110
+ export APIO_CONNECT_TOKEN=aw_...
111
+ export APIO_CONNECT_AUTHORIZATION=Bearer aw_...
112
+ export APIO_CONNECT_HEADER_NAME=X-Apiosk-Connect-Token
113
+ # Optional USDC-rail guardrails:
114
+ export APIO_WALLET_DAILY_LIMIT_USDC=100
115
+ export APIO_WALLET_PER_TX_LIMIT_USDC=1
116
+ ```
117
+
118
+ The `APIO_WALLET_*` limits only bound the **USDC** rail. The **SEPA mandate and
119
+ its threshold/age terms live server-side** against the same buyer account, so the
120
+ identical connect token transparently settles over SEPA incasso when USDC is
121
+ unavailable — no extra connect-string fields are required. An agent holding only
122
+ this connect token can therefore make paid calls with no on-chain balance; those
123
+ calls land on the SEPA ledger and are collected in the next batch.
124
+
125
+ ## Agent guidance
126
+
127
+ - Agents do **not** set up the mandate — that is a one-time human action in the
128
+ buyer portal.
129
+ - SEPA-backed calls succeed immediately even with no wallet balance and no x402
130
+ proof; settlement is deferred to the batch.
131
+ - Inspect outstanding (unbatched) SEPA balance and upcoming collections via the
132
+ buyer portal or `apiosk_get_credits_status`.
133
+ - For the machine-readable version of this doc, call `apiosk_help` with
134
+ `topic="rails"`.
package/dxt.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "apiosk-mcp",
3
+ "version": "1.3.0",
4
+ "description": "Official MCP server for browsing, paying for, and publishing Apiosk APIs",
5
+ "author": "Apiosk",
6
+ "license": "MIT",
7
+ "repository": "https://github.com/apiosk/apiosk-mcp",
8
+ "mcp": {
9
+ "command": "node",
10
+ "args": ["index.mjs"]
11
+ },
12
+ "tools": [
13
+ {
14
+ "name": "apiosk_explore",
15
+ "description": "Browse Apiosk listing groups before narrowing with search."
16
+ },
17
+ {
18
+ "name": "apiosk_search",
19
+ "description": "Search the Apiosk catalog and discover API-specific dynamic tools."
20
+ },
21
+ {
22
+ "name": "apiosk_payment_guide",
23
+ "description": "Guide for paying through the Apiosk gateway — buyer (how to settle a paid call) and provider (how to publish and get paid)."
24
+ },
25
+ {
26
+ "name": "apiosk_get_api",
27
+ "description": "Get full listing detail and agent metadata for a specific Apiosk slug."
28
+ },
29
+ {
30
+ "name": "apiosk_execute",
31
+ "description": "Fallback execute tool for any Apiosk API slug."
32
+ },
33
+ {
34
+ "name": "apiosk_wallet_create",
35
+ "description": "Create or import a local wallet for autonomous pay and publish flows."
36
+ },
37
+ {
38
+ "name": "apiosk_configure",
39
+ "description": "Show a structured wallet funding and Apiosk management menu, including QR output when available."
40
+ },
41
+ {
42
+ "name": "apiosk_publish_api",
43
+ "description": "Publish an API directly to Apiosk with signed wallet auth."
44
+ }
45
+ ]
46
+ }
package/index.mjs ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { createApioskMcpServer } from "./src/create-server.mjs";
4
+ import { createApioskMcpRuntime } from "./src/runtime.mjs";
5
+
6
+ const runtime = createApioskMcpRuntime({ enableLocalWallets: true });
7
+ const server = createApioskMcpServer({ runtime });
8
+ const transport = new StdioServerTransport();
9
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@apiosk/mcp",
3
+ "version": "1.3.0",
4
+ "description": "Official MCP server for browsing, paying for, and publishing Apiosk APIs",
5
+ "mcpName": "io.github.obcraft/apiosk-mcp",
6
+ "main": "index.mjs",
7
+ "exports": {
8
+ ".": "./src/create-server.mjs",
9
+ "./runtime": "./src/runtime.mjs",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "bin": {
13
+ "apiosk-mcp-server": "./index.mjs",
14
+ "apiosk-mcp": "./index.mjs",
15
+ "apiosk": "./index.mjs"
16
+ },
17
+ "scripts": {
18
+ "pack:check": "npm pack --dry-run",
19
+ "smoke:new-env": "bash scripts/smoke-new-env.sh",
20
+ "test:macbook-air": "bash scripts/test-macbook-air.sh",
21
+ "test:live": "bash scripts/test-live-url.sh",
22
+ "test": "node --test",
23
+ "dev": "node server.mjs",
24
+ "start": "node server.mjs"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "apiosk",
29
+ "api",
30
+ "model-context-protocol",
31
+ "x402",
32
+ "agent-tools",
33
+ "payments"
34
+ ],
35
+ "author": "Apiosk",
36
+ "license": "MIT",
37
+ "engines": {
38
+ "node": ">=20.0.0"
39
+ },
40
+ "type": "module",
41
+ "homepage": "https://apiosk.com",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/apiosk/mcp.git"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/apiosk/mcp/issues"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "files": [
53
+ "index.mjs",
54
+ "server.mjs",
55
+ "well-known.mjs",
56
+ "server.json",
57
+ "dxt.json",
58
+ "src",
59
+ "README.md",
60
+ "docs"
61
+ ],
62
+ "dependencies": {
63
+ "@apiosk/sdk": "^0.1.0",
64
+ "@modelcontextprotocol/sdk": "^1.27.1",
65
+ "express": "^5.2.1",
66
+ "qrcode": "^1.5.4",
67
+ "viem": "^2.47.0",
68
+ "zod": "^4.3.6"
69
+ },
70
+ "devDependencies": {
71
+ "@flydotio/dockerfile": "^0.7.10",
72
+ "@types/express": "^5.0.6",
73
+ "@types/node": "^25.5.0",
74
+ "ts-node": "^10.9.2",
75
+ "typescript": "^5.9.3"
76
+ }
77
+ }
package/server.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.obcraft/apiosk-mcp",
4
+ "title": "Apiosk",
5
+ "description": "Discover, pay for, execute, and publish Apiosk APIs through MCP.",
6
+ "version": "1.3.0",
7
+ "repository": {
8
+ "url": "https://github.com/obcraft/apiosk-mcp",
9
+ "source": "github"
10
+ },
11
+ "remotes": [
12
+ {
13
+ "type": "streamable-http",
14
+ "url": "https://mcp.apiosk.com/mcp"
15
+ }
16
+ ]
17
+ }
package/server.mjs ADDED
@@ -0,0 +1,373 @@
1
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
2
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ import {
4
+ SERVER_INFO,
5
+ createApioskMcpServer,
6
+ listApioskTools,
7
+ } from "./src/create-server.mjs";
8
+ import { createHostedOAuthSupport, resolveHostedMcpUrls } from "./src/oauth.mjs";
9
+ import { createApioskMcpRuntime } from "./src/runtime.mjs";
10
+ import {
11
+ resolveOpenAiAppsChallengeToken,
12
+ sendOpenAiAppsChallenge,
13
+ } from "./well-known.mjs";
14
+
15
+ const CONTROL_PLANE_BACKEND_URL = (
16
+ process.env.APIOSK_CONTROL_PLANE_BACKEND_URL ||
17
+ process.env.APIOSK_DASHBOARD_URL ||
18
+ "https://dashboard.apiosk.com"
19
+ ).replace(/\/+$/, "");
20
+ const OPENAI_APPS_CHALLENGE_TOKEN = resolveOpenAiAppsChallengeToken(process.env);
21
+ const OPENAI_APPS_CHALLENGE_PATH_PATTERN =
22
+ /^\/\.well-known\/openai-apps-challenge(?:\/\.well-known\/openai-apps-challenge)*\/?$/;
23
+
24
+ function normalizeControlPlanePath(pathname = "") {
25
+ const basePath = String(pathname || "")
26
+ .split("?")[0]
27
+ .replace(/\/+$/, "");
28
+
29
+ if (!basePath) {
30
+ return "/";
31
+ }
32
+
33
+ if (basePath === "/api") {
34
+ return "/api";
35
+ }
36
+
37
+ if (basePath.startsWith("/api/")) {
38
+ return basePath;
39
+ }
40
+
41
+ if (basePath.startsWith("/")) {
42
+ return `/api${basePath}`;
43
+ }
44
+
45
+ return `/api/${basePath}`;
46
+ }
47
+
48
+ function shouldProxyControlPlanePath(pathname = "") {
49
+ const normalizedPath = normalizeControlPlanePath(pathname);
50
+ return (
51
+ normalizedPath === "/api/auth/mcp-sign-in" ||
52
+ normalizedPath === "/api/auth/mcp-sign-up" ||
53
+ normalizedPath.startsWith("/api/credits/") ||
54
+ normalizedPath === "/api/agent-wallets" ||
55
+ normalizedPath.startsWith("/api/agent-wallets/")
56
+ );
57
+ }
58
+
59
+ async function proxyControlPlaneRequest(req, res) {
60
+ const targetUrl = new URL(req.originalUrl || req.url || "/", CONTROL_PLANE_BACKEND_URL);
61
+ const headers = new Headers();
62
+
63
+ for (const [key, value] of Object.entries(req.headers || {})) {
64
+ if (value == null) continue;
65
+ if (["host", "content-length", "connection"].includes(key.toLowerCase())) continue;
66
+ if (Array.isArray(value)) {
67
+ for (const item of value) headers.append(key, item);
68
+ } else {
69
+ headers.set(key, value);
70
+ }
71
+ }
72
+
73
+ let body = undefined;
74
+ if (!["GET", "HEAD"].includes(req.method.toUpperCase())) {
75
+ if (typeof req.body === "string") {
76
+ body = req.body;
77
+ } else if (req.body !== undefined && req.body !== null) {
78
+ headers.set("content-type", headers.get("content-type") || "application/json");
79
+ body = JSON.stringify(req.body);
80
+ }
81
+ }
82
+
83
+ const response = await fetch(targetUrl, {
84
+ method: req.method,
85
+ headers,
86
+ body,
87
+ redirect: "manual",
88
+ });
89
+
90
+ res.status(response.status);
91
+ response.headers.forEach((value, key) => {
92
+ if (["content-encoding", "transfer-encoding", "connection"].includes(key.toLowerCase())) return;
93
+ res.setHeader(key, value);
94
+ });
95
+
96
+ const text = await response.text();
97
+ res.send(text);
98
+ }
99
+
100
+ function resolvePublicMcpUrl(req) {
101
+ const forwardedProto = String(req.headers["x-forwarded-proto"] || "").split(",")[0].trim();
102
+ const proto = forwardedProto || req.protocol || "https";
103
+ const host = req.headers.host || "mcp.apiosk.com";
104
+ return `${proto}://${host}/mcp`;
105
+ }
106
+
107
+ function renderMcpWelcomeHtml(mcpUrl) {
108
+ return `<!doctype html>
109
+ <html lang="en">
110
+ <head>
111
+ <meta charset="utf-8" />
112
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
113
+ <meta name="robots" content="noindex" />
114
+ <title>Apiosk MCP</title>
115
+ <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; }
149
+ </style>
150
+ </head>
151
+ <body>
152
+ <main>
153
+ <div class="card">
154
+ <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>
173
+
174
+ <h2>Connect from Claude Code</h2>
175
+ <code>claude mcp add --transport http apiosk ${mcpUrl}</code>
176
+
177
+ <div class="links">
178
+ <a href="/health">Health</a>
179
+ <a href="https://dashboard.apiosk.com" target="_blank" rel="noopener">Dashboard</a>
180
+ <a href="https://github.com/obcraft/apiosk-mcp" target="_blank" rel="noopener">Docs &amp; source</a>
181
+ </div>
182
+ <footer>Apiosk MCP &middot; ${SERVER_INFO.name} v${SERVER_INFO.version}</footer>
183
+ </div>
184
+ </main>
185
+ </body>
186
+ </html>`;
187
+ }
188
+
189
+ // Browsers (Accept: text/html) get a friendly welcome page; other non-protocol
190
+ // callers get a JSON welcome. MCP protocol clients (Accept: text/event-stream)
191
+ // are handled separately with the spec-compliant 405.
192
+ function sendMcpWelcome(req, res) {
193
+ const mcpUrl = resolvePublicMcpUrl(req);
194
+ const accept = String(req.headers.accept || "");
195
+
196
+ if (accept.includes("text/html")) {
197
+ res.status(200).type("html").send(renderMcpWelcomeHtml(mcpUrl));
198
+ return;
199
+ }
200
+
201
+ res.status(200).json({
202
+ name: "Apiosk MCP",
203
+ 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.",
206
+ transport: "streamable-http",
207
+ endpoint: mcpUrl,
208
+ connect: {
209
+ claude_code: `claude mcp add --transport http apiosk ${mcpUrl}`,
210
+ },
211
+ docs: "https://github.com/obcraft/apiosk-mcp",
212
+ health: "/health",
213
+ });
214
+ }
215
+
216
+ // Public Fly deployment must accept the Fly hostname instead of localhost-only
217
+ // host validation defaults.
218
+ const app = createMcpExpressApp({ host: "0.0.0.0" });
219
+ const port = Number(process.env.PORT || 3000);
220
+ const { issuerUrl, mcpServerUrl } = resolveHostedMcpUrls({
221
+ env: process.env,
222
+ port,
223
+ });
224
+ const runtime = createApioskMcpRuntime({
225
+ enableLocalWallets: process.env.APIOSK_ENABLE_LOCAL_WALLETS === "true",
226
+ hostedAuthEnabled: true,
227
+ });
228
+ const hostedOAuth = createHostedOAuthSupport({
229
+ env: process.env,
230
+ controlPlaneBaseUrl: CONTROL_PLANE_BACKEND_URL,
231
+ issuerUrl,
232
+ mcpServerUrl,
233
+ appName: "Apiosk",
234
+ resourceName: "Apiosk MCP",
235
+ });
236
+ const mcpAuthMiddleware = hostedOAuth.createMcpAuthMiddleware(runtime);
237
+
238
+ app.use(hostedOAuth.metadataRouter);
239
+ app.use(new URL(hostedOAuth.oauthMetadata.authorization_endpoint).pathname, hostedOAuth.authorizationRouter);
240
+ app.use(new URL(hostedOAuth.oauthMetadata.token_endpoint).pathname, hostedOAuth.tokenRouter);
241
+ if (hostedOAuth.oauthMetadata.registration_endpoint) {
242
+ app.use(
243
+ new URL(hostedOAuth.oauthMetadata.registration_endpoint).pathname,
244
+ hostedOAuth.registrationRouter
245
+ );
246
+ }
247
+
248
+ app.get(OPENAI_APPS_CHALLENGE_PATH_PATTERN, (req, res) => {
249
+ return sendOpenAiAppsChallenge(res, OPENAI_APPS_CHALLENGE_TOKEN);
250
+ });
251
+
252
+ app.all("/api/*path", async (req, res) => {
253
+ try {
254
+ const pathname = req.path || req.originalUrl || "";
255
+ if (!shouldProxyControlPlanePath(pathname)) {
256
+ return res.status(404).json({
257
+ error: "not_found",
258
+ message: "Unknown MCP control-plane route.",
259
+ status: 404,
260
+ });
261
+ }
262
+
263
+ await proxyControlPlaneRequest(req, res);
264
+ } catch (error) {
265
+ res.status(502).json({
266
+ error: "bad_gateway",
267
+ message: error instanceof Error ? error.message : String(error),
268
+ status: 502,
269
+ });
270
+ }
271
+ });
272
+
273
+ app.get("/health", async (req, res) => {
274
+ try {
275
+ const tools = await listApioskTools({ runtime });
276
+ res.json({
277
+ status: "ok",
278
+ server: SERVER_INFO,
279
+ tool_count: tools.length,
280
+ });
281
+ } catch (error) {
282
+ res.status(503).json({
283
+ status: "degraded",
284
+ server: SERVER_INFO,
285
+ error: error instanceof Error ? error.message : String(error),
286
+ });
287
+ }
288
+ });
289
+
290
+ app.post("/mcp", mcpAuthMiddleware, async (req, res) => {
291
+ const server = createApioskMcpServer({ runtime });
292
+ const transport = new StreamableHTTPServerTransport({
293
+ sessionIdGenerator: undefined,
294
+ });
295
+
296
+ try {
297
+ await server.connect(transport);
298
+ await transport.handleRequest(req, res, req.body);
299
+
300
+ res.on("close", () => {
301
+ transport.close().catch(() => {});
302
+ server.close().catch(() => {});
303
+ });
304
+ } catch (error) {
305
+ console.error("Error handling MCP request:", error);
306
+
307
+ if (!res.headersSent) {
308
+ return res.status(500).json({
309
+ jsonrpc: "2.0",
310
+ id: null,
311
+ error: {
312
+ code: -32603,
313
+ message: error instanceof Error ? error.message : String(error),
314
+ },
315
+ });
316
+ }
317
+ } finally {
318
+ if (res.writableEnded) {
319
+ await transport.close().catch(() => {});
320
+ await server.close().catch(() => {});
321
+ }
322
+ }
323
+ });
324
+
325
+ app.get("/mcp", (req, res) => {
326
+ const accept = String(req.headers.accept || "");
327
+
328
+ // MCP Streamable HTTP clients open the optional SSE stream via a GET with
329
+ // Accept: text/event-stream. This stateless server does not provide that
330
+ // stream, so keep the spec-compliant 405 for protocol clients.
331
+ if (accept.includes("text/event-stream")) {
332
+ return res.status(405).json({
333
+ jsonrpc: "2.0",
334
+ id: null,
335
+ error: {
336
+ code: -32000,
337
+ message: "Method not allowed.",
338
+ },
339
+ });
340
+ }
341
+
342
+ // Humans navigating here in a browser (or curling the URL) get a welcome.
343
+ return sendMcpWelcome(req, res);
344
+ });
345
+
346
+ app.delete("/mcp", (req, res) => {
347
+ res.status(405).json({
348
+ jsonrpc: "2.0",
349
+ id: null,
350
+ error: {
351
+ code: -32000,
352
+ message: "Method not allowed.",
353
+ },
354
+ });
355
+ });
356
+
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`);
361
+ console.log(`OAuth issuer: ${issuerUrl.href}`);
362
+ console.log(`OAuth protected-resource metadata: ${hostedOAuth.resourceMetadataUrl}`);
363
+ try {
364
+ const tools = await listApioskTools({ runtime });
365
+ console.log(`Loaded ${tools.length} tools from the Apiosk catalog.`);
366
+ } catch (error) {
367
+ console.warn(
368
+ `Unable to prefetch Apiosk catalog on startup: ${
369
+ error instanceof Error ? error.message : String(error)
370
+ }`
371
+ );
372
+ }
373
+ });
@@ -0,0 +1,37 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import {
3
+ CallToolRequestSchema,
4
+ ListToolsRequestSchema,
5
+ } from "@modelcontextprotocol/sdk/types.js";
6
+ import { createApioskMcpRuntime } from "./runtime.mjs";
7
+
8
+ export const SERVER_INFO = {
9
+ name: "apiosk-mcp",
10
+ version: "1.3.0",
11
+ };
12
+
13
+ function resolveRuntime(options = {}) {
14
+ return options.runtime || createApioskMcpRuntime(options);
15
+ }
16
+
17
+ export async function listApioskTools(options = {}) {
18
+ return resolveRuntime(options).listTools(options.authInfo);
19
+ }
20
+
21
+ export function createApioskMcpServer(options = {}) {
22
+ const runtime = resolveRuntime(options);
23
+ const server = new Server(
24
+ SERVER_INFO,
25
+ { capabilities: { tools: {} } }
26
+ );
27
+
28
+ server.setRequestHandler(ListToolsRequestSchema, async (_request, extra) => ({
29
+ tools: await runtime.listTools(extra.authInfo),
30
+ }));
31
+
32
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
33
+ return runtime.callTool(request.params.name, request.params.arguments || {}, extra.authInfo);
34
+ });
35
+
36
+ return server;
37
+ }