@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.
- package/README.md +590 -0
- package/docs/marketplace-submission-2026-04-08.md +92 -0
- package/docs/sepa-rail.md +134 -0
- package/dxt.json +46 -0
- package/index.mjs +9 -0
- package/package.json +77 -0
- package/server.json +17 -0
- package/server.mjs +373 -0
- package/src/create-server.mjs +37 -0
- package/src/funding-options.mjs +255 -0
- package/src/gateway-management.mjs +107 -0
- package/src/listing-metadata.mjs +287 -0
- package/src/local-config.mjs +256 -0
- package/src/oauth.mjs +1015 -0
- package/src/payment-guidance.mjs +307 -0
- package/src/runtime.mjs +3090 -0
- package/src/wallet-store.mjs +476 -0
- package/well-known.mjs +11 -0
|
@@ -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 — 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 & 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 & source</a>
|
|
181
|
+
</div>
|
|
182
|
+
<footer>Apiosk MCP · ${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
|
+
}
|