@apiosk/mcp 1.3.1 → 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.
- package/README.md +84 -537
- package/assets/brand/apiosk-a-20260921.png +0 -0
- package/assets/brand/apple-touch-icon.png +0 -0
- package/assets/brand/favicon-dark.ico +0 -0
- package/assets/brand/favicon-light.ico +0 -0
- package/assets/brand/favicon.ico +0 -0
- package/assets/brand/icon-192.png +0 -0
- package/assets/brand/icon-512.png +0 -0
- package/assets/brand/icon-maskable-512.png +0 -0
- package/assets/brand/inter-OFL.txt +93 -0
- package/assets/brand/inter-latin-400-normal.woff2 +0 -0
- package/assets/brand/inter-latin-500-normal.woff2 +0 -0
- package/assets/brand/inter-latin-600-normal.woff2 +0 -0
- package/assets/brand/mark-20260905-transparent.svg +1 -0
- package/assets/brand/mark-20260918.svg +1 -0
- package/assets/brand/mark-black-20260918.svg +1 -0
- package/assets/brand/mark-dark-20260905-transparent.png +0 -0
- package/assets/brand/mark-dark-20260918.png +0 -0
- package/assets/brand/mark-dark-96.png +0 -0
- package/assets/brand/mark-light-20260905-transparent.png +0 -0
- package/assets/brand/mark-light-20260918.png +0 -0
- package/assets/brand/mark-light-96.png +0 -0
- package/assets/brand/wordmark-black-320.png +0 -0
- package/assets/brand/wordmark-white-320.png +0 -0
- package/docs/branding.md +11 -0
- package/docs/marketplace-submission-2026-04-08.md +4 -0
- package/docs/marketplace-submission-2026-08-20.md +157 -0
- package/docs/openai-plugin-submission-2026-09-05.md +143 -0
- package/docs/sepa-rail.md +13 -13
- package/dxt.json +26 -22
- package/index.mjs +3 -1
- package/logo-optimized-light.png +0 -0
- package/package.json +15 -13
- package/plugin/apiosk/.codex-plugin/plugin.json +45 -0
- package/plugin/apiosk/.mcp.json +8 -0
- package/plugin/apiosk/assets/icon-dark.png +0 -0
- package/plugin/apiosk/assets/icon.png +0 -0
- package/plugin/apiosk/assets/icon.svg +1 -0
- package/plugin/apiosk/assets/logo.png +0 -0
- package/plugin/apiosk/skills/apiosk/SKILL.md +33 -0
- package/plugin/apiosk/skills/apiosk/agents/openai.yaml +12 -0
- package/server.json +98 -9
- package/server.mjs +258 -71
- package/src/approval-feedback.mjs +21 -0
- package/src/brand-routes.mjs +28 -0
- package/src/create-server.mjs +168 -9
- package/src/display-money.mjs +20 -0
- package/src/display-text.mjs +67 -0
- package/src/gateway-client.mjs +42 -0
- package/src/gateway-v2-ask.mjs +50 -0
- package/src/gateway-v2-card-account.mjs +23 -0
- package/src/gateway-v2-card-actions.mjs +67 -0
- package/src/gateway-v2-card-answer-text.mjs +130 -0
- package/src/gateway-v2-card-answer.mjs +68 -0
- package/src/gateway-v2-card-blocks.mjs +166 -0
- package/src/gateway-v2-card-body.mjs +259 -0
- package/src/gateway-v2-card-budget.mjs +21 -0
- package/src/gateway-v2-card-cbs.mjs +56 -0
- package/src/gateway-v2-card-choices.mjs +4 -0
- package/src/gateway-v2-card-clarification.mjs +25 -0
- package/src/gateway-v2-card-compact.mjs +34 -0
- package/src/gateway-v2-card-events.mjs +41 -0
- package/src/gateway-v2-card-presentation.mjs +125 -0
- package/src/gateway-v2-card-research.mjs +63 -0
- package/src/gateway-v2-card-result.mjs +32 -0
- package/src/gateway-v2-card-search.mjs +39 -0
- package/src/gateway-v2-card-sources.mjs +32 -0
- package/src/gateway-v2-card-style.mjs +76 -0
- package/src/gateway-v2-card-verdict.mjs +66 -0
- package/src/gateway-v2-card.mjs +91 -0
- package/src/gateway-v2-contracts.json +66 -0
- package/src/gateway-v2-instructions.md +113 -0
- package/src/gateway-v2-recovery.mjs +18 -0
- package/src/gateway-v2-report-links.mjs +14 -0
- package/src/gateway-v2-workflows.mjs +17 -0
- package/src/gateway-v2.mjs +179 -0
- package/src/oauth.mjs +603 -364
- package/src/observability.mjs +210 -0
- package/src/result-presentation.mjs +9 -0
- package/src/runtime.mjs +24 -3091
- package/src/settlement-disclosure.mjs +26 -0
- package/src/source-groups.mjs +91 -0
- package/src/source-value-format.mjs +25 -0
- package/src/tool-result.mjs +20 -0
- package/src/ui-bridge.mjs +185 -0
- package/src/well-known-routes.mjs +132 -0
- package/src/funding-options.mjs +0 -255
- package/src/gateway-management.mjs +0 -107
- package/src/listing-metadata.mjs +0 -287
- package/src/local-config.mjs +0 -256
- package/src/payment-guidance.mjs +0 -307
- package/src/wallet-store.mjs +0 -476
package/server.json
CHANGED
|
@@ -1,26 +1,97 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.obcraft/apiosk-mcp",
|
|
4
|
-
"title": "Apiosk",
|
|
5
|
-
"description": "
|
|
6
|
-
"version": "
|
|
4
|
+
"title": "Apiosk Connect",
|
|
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://apiosk.com/
|
|
10
|
+
"src": "https://mcp.apiosk.com/brand/apiosk-a-20260921.png",
|
|
11
|
+
"mimeType": "image/png",
|
|
12
|
+
"sizes": [
|
|
13
|
+
"1254x1254"
|
|
14
|
+
],
|
|
15
|
+
"theme": "light"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"src": "https://mcp.apiosk.com/brand/mark-20260918.svg",
|
|
11
19
|
"mimeType": "image/svg+xml",
|
|
12
|
-
"sizes": [
|
|
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"
|
|
13
31
|
},
|
|
14
32
|
{
|
|
15
|
-
"src": "https://apiosk.com/
|
|
33
|
+
"src": "https://mcp.apiosk.com/brand/mark-dark-20260918.png",
|
|
16
34
|
"mimeType": "image/png",
|
|
17
|
-
"sizes": [
|
|
35
|
+
"sizes": [
|
|
36
|
+
"512x512"
|
|
37
|
+
],
|
|
38
|
+
"theme": "dark"
|
|
18
39
|
}
|
|
19
40
|
],
|
|
20
41
|
"repository": {
|
|
21
42
|
"url": "https://github.com/obcraft/apiosk-mcp",
|
|
22
43
|
"source": "github"
|
|
23
44
|
},
|
|
45
|
+
"packages": [
|
|
46
|
+
{
|
|
47
|
+
"registryType": "npm",
|
|
48
|
+
"identifier": "@apiosk/mcp",
|
|
49
|
+
"version": "2.0.0",
|
|
50
|
+
"transport": {
|
|
51
|
+
"type": "stdio"
|
|
52
|
+
},
|
|
53
|
+
"environmentVariables": [
|
|
54
|
+
{
|
|
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,
|
|
58
|
+
"isSecret": true,
|
|
59
|
+
"format": "string"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
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.",
|
|
64
|
+
"isRequired": false,
|
|
65
|
+
"isSecret": false,
|
|
66
|
+
"format": "string"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"registryType": "pypi",
|
|
72
|
+
"identifier": "apiosk-mcp",
|
|
73
|
+
"version": "2.0.0",
|
|
74
|
+
"transport": {
|
|
75
|
+
"type": "stdio"
|
|
76
|
+
},
|
|
77
|
+
"environmentVariables": [
|
|
78
|
+
{
|
|
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,
|
|
82
|
+
"isSecret": true,
|
|
83
|
+
"format": "string"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
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.",
|
|
88
|
+
"isRequired": false,
|
|
89
|
+
"isSecret": false,
|
|
90
|
+
"format": "string"
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
],
|
|
24
95
|
"remotes": [
|
|
25
96
|
{
|
|
26
97
|
"type": "streamable-http",
|
|
@@ -29,8 +100,26 @@
|
|
|
29
100
|
],
|
|
30
101
|
"_meta": {
|
|
31
102
|
"com.apiosk": {
|
|
32
|
-
"tags": [
|
|
33
|
-
|
|
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
|
+
]
|
|
34
123
|
}
|
|
35
124
|
}
|
|
36
125
|
}
|
package/server.mjs
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
|
+
import { registerBrandRoutes, BRAND_LINKS } from "./src/brand-routes.mjs";
|
|
1
2
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
2
4
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
3
6
|
import {
|
|
4
|
-
|
|
7
|
+
resolveServerPresentation,
|
|
5
8
|
createApioskMcpServer,
|
|
6
9
|
listApioskTools,
|
|
7
10
|
} from "./src/create-server.mjs";
|
|
8
|
-
import {
|
|
11
|
+
import { V2_RESOURCE } from "./src/gateway-v2.mjs";
|
|
12
|
+
import {
|
|
13
|
+
createHostedOAuthSupport,
|
|
14
|
+
resolveHostedMcpUrls,
|
|
15
|
+
} from "./src/oauth.mjs";
|
|
9
16
|
import { createApioskMcpRuntime } from "./src/runtime.mjs";
|
|
17
|
+
import { openSession, closeSession } from "./src/observability.mjs";
|
|
10
18
|
import {
|
|
11
19
|
resolveOpenAiAppsChallengeToken,
|
|
12
20
|
sendOpenAiAppsChallenge,
|
|
13
21
|
} from "./well-known.mjs";
|
|
22
|
+
import { buildDiscoveryDocument } from "./src/well-known-routes.mjs";
|
|
23
|
+
import {
|
|
24
|
+
SETTLEMENT_DISCLOSURE_PATH,
|
|
25
|
+
createSettlementDisclosurePage,
|
|
26
|
+
} from "./src/settlement-disclosure.mjs";
|
|
27
|
+
|
|
28
|
+
const { info: SERVER_INFO, description: SERVER_DESCRIPTION, instructions: SERVER_INSTRUCTIONS } = resolveServerPresentation();
|
|
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 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.
|
|
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,75 +127,41 @@ 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>
|
|
131
|
+
${BRAND_LINKS}
|
|
115
132
|
<style>
|
|
116
|
-
:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
.
|
|
126
|
-
|
|
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; }
|
|
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}
|
|
149
144
|
</style>
|
|
150
145
|
</head>
|
|
151
146
|
<body>
|
|
152
147
|
<main>
|
|
153
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>
|
|
154
150
|
<span class="badge">Model Context Protocol</span>
|
|
155
|
-
<h1>
|
|
156
|
-
<p class="lead">
|
|
157
|
-
|
|
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>
|
|
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>
|
|
173
154
|
|
|
174
|
-
<h2>
|
|
175
|
-
<code
|
|
155
|
+
<h2>Secure MCP endpoint</h2>
|
|
156
|
+
<code class="endpoint">${mcpUrl}</code>
|
|
157
|
+
<details><summary>Manual setup</summary><code>claude mcp add --transport http apiosk ${mcpUrl}</code><code>${mcpUrl.replace(/\/mcp$/, "/sse")}</code></details>
|
|
176
158
|
|
|
177
159
|
<div class="links">
|
|
178
160
|
<a href="/health">Health</a>
|
|
179
161
|
<a href="https://dashboard.apiosk.com" target="_blank" rel="noopener">Dashboard</a>
|
|
180
162
|
<a href="https://github.com/obcraft/apiosk-mcp" target="_blank" rel="noopener">Docs & source</a>
|
|
181
163
|
</div>
|
|
182
|
-
<footer>Apiosk
|
|
164
|
+
<footer>Apiosk Connect · ${SERVER_INFO.name} v${SERVER_INFO.version}</footer>
|
|
183
165
|
</div>
|
|
184
166
|
</main>
|
|
185
167
|
</body>
|
|
@@ -199,12 +181,12 @@ function sendMcpWelcome(req, res) {
|
|
|
199
181
|
}
|
|
200
182
|
|
|
201
183
|
res.status(200).json({
|
|
202
|
-
name: "Apiosk
|
|
184
|
+
name: "Apiosk Connect",
|
|
203
185
|
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.",
|
|
186
|
+
description: SERVER_DESCRIPTION,
|
|
206
187
|
transport: "streamable-http",
|
|
207
188
|
endpoint: mcpUrl,
|
|
189
|
+
legacy_sse_endpoint: mcpUrl.replace(/\/mcp$/, "/sse"),
|
|
208
190
|
connect: {
|
|
209
191
|
claude_code: `claude mcp add --transport http apiosk ${mcpUrl}`,
|
|
210
192
|
},
|
|
@@ -215,26 +197,48 @@ function sendMcpWelcome(req, res) {
|
|
|
215
197
|
|
|
216
198
|
// Public Fly deployment must accept the Fly hostname instead of localhost-only
|
|
217
199
|
// host validation defaults.
|
|
218
|
-
const
|
|
200
|
+
const bindHost = process.env.APIOSK_MCP_BIND_HOST || "0.0.0.0";
|
|
201
|
+
const app = createMcpExpressApp({ host: bindHost });
|
|
202
|
+
// Fly terminates TLS and adds one trusted proxy hop. Without this,
|
|
203
|
+
// express-rate-limit rejects X-Forwarded-For and can surface as MCP -32603.
|
|
204
|
+
app.set("trust proxy", 1);
|
|
219
205
|
const port = Number(process.env.PORT || 3000);
|
|
220
206
|
const { issuerUrl, mcpServerUrl } = resolveHostedMcpUrls({
|
|
221
207
|
env: process.env,
|
|
222
208
|
port,
|
|
223
209
|
});
|
|
224
210
|
const runtime = createApioskMcpRuntime({
|
|
225
|
-
|
|
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.
|
|
226
213
|
hostedAuthEnabled: true,
|
|
227
214
|
});
|
|
228
215
|
const hostedOAuth = createHostedOAuthSupport({
|
|
229
216
|
env: process.env,
|
|
230
|
-
controlPlaneBaseUrl: CONTROL_PLANE_BACKEND_URL,
|
|
231
217
|
issuerUrl,
|
|
232
218
|
mcpServerUrl,
|
|
233
219
|
appName: "Apiosk",
|
|
234
|
-
resourceName: "Apiosk
|
|
220
|
+
resourceName: "Apiosk Connect",
|
|
235
221
|
});
|
|
236
222
|
const mcpAuthMiddleware = hostedOAuth.createMcpAuthMiddleware(runtime);
|
|
237
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
|
+
|
|
238
242
|
app.use(hostedOAuth.metadataRouter);
|
|
239
243
|
app.use(new URL(hostedOAuth.oauthMetadata.authorization_endpoint).pathname, hostedOAuth.authorizationRouter);
|
|
240
244
|
app.use(new URL(hostedOAuth.oauthMetadata.token_endpoint).pathname, hostedOAuth.tokenRouter);
|
|
@@ -244,11 +248,62 @@ if (hostedOAuth.oauthMetadata.registration_endpoint) {
|
|
|
244
248
|
hostedOAuth.registrationRouter
|
|
245
249
|
);
|
|
246
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));
|
|
247
253
|
|
|
248
254
|
app.get(OPENAI_APPS_CHALLENGE_PATH_PATTERN, (req, res) => {
|
|
249
255
|
return sendOpenAiAppsChallenge(res, OPENAI_APPS_CHALLENGE_TOKEN);
|
|
250
256
|
});
|
|
251
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.
|
|
267
|
+
app.get("/logo-optimized-light.png", (req, res) => {
|
|
268
|
+
res.setHeader("cache-control", "public, max-age=0, must-revalidate");
|
|
269
|
+
res.type("png").sendFile(fileURLToPath(new URL("./logo-optimized-light.png", import.meta.url)));
|
|
270
|
+
});
|
|
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
|
+
|
|
300
|
+
app.get(SETTLEMENT_DISCLOSURE_PATH, (_req, res) => {
|
|
301
|
+
res
|
|
302
|
+
.setHeader("cache-control", "public, max-age=300")
|
|
303
|
+
.type("html")
|
|
304
|
+
.send(createSettlementDisclosurePage());
|
|
305
|
+
});
|
|
306
|
+
|
|
252
307
|
app.all("/api/*path", async (req, res) => {
|
|
253
308
|
try {
|
|
254
309
|
const pathname = req.path || req.originalUrl || "";
|
|
@@ -270,6 +325,70 @@ app.all("/api/*path", async (req, res) => {
|
|
|
270
325
|
}
|
|
271
326
|
});
|
|
272
327
|
|
|
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) => {
|
|
338
|
+
try {
|
|
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],
|
|
367
|
+
});
|
|
368
|
+
} catch (error) {
|
|
369
|
+
res.status(500).json({
|
|
370
|
+
error: "server_card_unavailable",
|
|
371
|
+
message: error instanceof Error ? error.message : String(error),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Machine-readable index of every paid x402 route published through Apiosk,
|
|
377
|
+
// reshaped from the gateway's /.well-known/x402 document (60s cache).
|
|
378
|
+
app.get(["/.well-known/apiosk-routes.json", "/discovery"], async (req, res) => {
|
|
379
|
+
try {
|
|
380
|
+
const document = await buildDiscoveryDocument({ env: process.env });
|
|
381
|
+
res.setHeader("cache-control", "public, max-age=60");
|
|
382
|
+
res.json(document);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
res.status(502).json({
|
|
385
|
+
error: "bad_gateway",
|
|
386
|
+
message: error instanceof Error ? error.message : String(error),
|
|
387
|
+
status: 502,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
273
392
|
app.get("/health", async (req, res) => {
|
|
274
393
|
try {
|
|
275
394
|
const tools = await listApioskTools({ runtime });
|
|
@@ -288,7 +407,7 @@ app.get("/health", async (req, res) => {
|
|
|
288
407
|
});
|
|
289
408
|
|
|
290
409
|
app.post("/mcp", mcpAuthMiddleware, async (req, res) => {
|
|
291
|
-
const server = createApioskMcpServer({ runtime });
|
|
410
|
+
const server = createApioskMcpServer({ runtime, legacyUiMime: /openai|chatgpt/i.test(req.headers["user-agent"] || "") });
|
|
292
411
|
const transport = new StreamableHTTPServerTransport({
|
|
293
412
|
sessionIdGenerator: undefined,
|
|
294
413
|
});
|
|
@@ -354,10 +473,78 @@ app.delete("/mcp", (req, res) => {
|
|
|
354
473
|
});
|
|
355
474
|
});
|
|
356
475
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
476
|
+
// Legacy HTTP+SSE transport (protocol version 2024-11-05), kept alongside
|
|
477
|
+
// Streamable HTTP for older MCP clients. Current ChatGPT and Claude installs
|
|
478
|
+
// should use the preferred /mcp Streamable HTTP endpoint.
|
|
479
|
+
const sseTransports = new Map();
|
|
480
|
+
|
|
481
|
+
app.get("/sse", async (req, res) => {
|
|
482
|
+
const server = createApioskMcpServer({ runtime });
|
|
483
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
484
|
+
sseTransports.set(transport.sessionId, transport);
|
|
485
|
+
|
|
486
|
+
// Observability: record the SSE connection (the "who's connected / installs"
|
|
487
|
+
// roster) and mark it closed on disconnect. Fire-and-forget.
|
|
488
|
+
openSession(process.env, {
|
|
489
|
+
sessionId: transport.sessionId,
|
|
490
|
+
transport: "sse",
|
|
491
|
+
ip: req.ip,
|
|
492
|
+
userAgent: req.headers["user-agent"],
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
res.on("close", () => {
|
|
496
|
+
sseTransports.delete(transport.sessionId);
|
|
497
|
+
closeSession(process.env, transport.sessionId);
|
|
498
|
+
transport.close().catch(() => {});
|
|
499
|
+
server.close().catch(() => {});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
await server.connect(transport);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
console.error("Error establishing /sse stream:", error);
|
|
506
|
+
sseTransports.delete(transport.sessionId);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
app.post("/messages", mcpAuthMiddleware, async (req, res) => {
|
|
511
|
+
const sessionId = typeof req.query.sessionId === "string" ? req.query.sessionId : "";
|
|
512
|
+
const transport = sseTransports.get(sessionId);
|
|
513
|
+
|
|
514
|
+
if (!transport) {
|
|
515
|
+
res.status(400).json({
|
|
516
|
+
jsonrpc: "2.0",
|
|
517
|
+
id: null,
|
|
518
|
+
error: {
|
|
519
|
+
code: -32000,
|
|
520
|
+
message: "Bad Request: No SSE session found for the given sessionId.",
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
await transport.handlePostMessage(req, res, req.body);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
console.error("Error handling /messages request:", error);
|
|
530
|
+
if (!res.headersSent) {
|
|
531
|
+
res.status(500).json({
|
|
532
|
+
jsonrpc: "2.0",
|
|
533
|
+
id: null,
|
|
534
|
+
error: {
|
|
535
|
+
code: -32603,
|
|
536
|
+
message: error instanceof Error ? error.message : String(error),
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
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`);
|
|
361
548
|
console.log(`OAuth issuer: ${issuerUrl.href}`);
|
|
362
549
|
console.log(`OAuth protected-resource metadata: ${hostedOAuth.resourceMetadataUrl}`);
|
|
363
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
|
+
}
|