@apiosk/mcp 1.7.0 → 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 +83 -612
- 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/dxt.json +26 -22
- package/index.mjs +3 -1
- package/logo-optimized-light.png +0 -0
- package/package.json +14 -14
- 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 +58 -24
- package/server.mjs +136 -151
- package/src/approval-feedback.mjs +21 -0
- package/src/brand-routes.mjs +28 -0
- package/src/create-server.mjs +160 -46
- 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 +402 -1949
- package/src/observability.mjs +19 -3
- package/src/result-presentation.mjs +9 -0
- package/src/runtime.mjs +24 -3670
- 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/assets/wallet-accounts.mjs +0 -20513
- package/src/assets/walletconnect-provider.mjs +0 -6319
- package/src/discovery.mjs +0 -929
- package/src/external-fetch.mjs +0 -203
- package/src/funding-options.mjs +0 -255
- package/src/gateway-management.mjs +0 -107
- package/src/hosted-payment.mjs +0 -552
- package/src/hosted-wallets.mjs +0 -530
- package/src/listing-metadata.mjs +0 -287
- package/src/local-config.mjs +0 -256
- package/src/payment-guidance.mjs +0 -298
- package/src/publisher.mjs +0 -1288
- package/src/result-canvas.mjs +0 -16
- package/src/source-registry.mjs +0 -215
- package/src/wallet-store.mjs +0 -476
- package/src/x402-inspect.mjs +0 -361
package/server.json
CHANGED
|
@@ -2,24 +2,40 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.obcraft/apiosk-mcp",
|
|
4
4
|
"title": "Apiosk Connect",
|
|
5
|
-
"description": "
|
|
6
|
-
"version": "
|
|
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://mcp.apiosk.com/
|
|
10
|
+
"src": "https://mcp.apiosk.com/brand/apiosk-a-20260921.png",
|
|
11
11
|
"mimeType": "image/png",
|
|
12
|
-
"sizes": [
|
|
12
|
+
"sizes": [
|
|
13
|
+
"1254x1254"
|
|
14
|
+
],
|
|
15
|
+
"theme": "light"
|
|
13
16
|
},
|
|
14
17
|
{
|
|
15
|
-
"src": "https://apiosk.com/
|
|
18
|
+
"src": "https://mcp.apiosk.com/brand/mark-20260918.svg",
|
|
16
19
|
"mimeType": "image/svg+xml",
|
|
17
|
-
"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"
|
|
18
31
|
},
|
|
19
32
|
{
|
|
20
|
-
"src": "https://apiosk.com/
|
|
33
|
+
"src": "https://mcp.apiosk.com/brand/mark-dark-20260918.png",
|
|
21
34
|
"mimeType": "image/png",
|
|
22
|
-
"sizes": [
|
|
35
|
+
"sizes": [
|
|
36
|
+
"512x512"
|
|
37
|
+
],
|
|
38
|
+
"theme": "dark"
|
|
23
39
|
}
|
|
24
40
|
],
|
|
25
41
|
"repository": {
|
|
@@ -30,23 +46,23 @@
|
|
|
30
46
|
{
|
|
31
47
|
"registryType": "npm",
|
|
32
48
|
"identifier": "@apiosk/mcp",
|
|
33
|
-
"version": "
|
|
49
|
+
"version": "2.0.0",
|
|
34
50
|
"transport": {
|
|
35
51
|
"type": "stdio"
|
|
36
52
|
},
|
|
37
53
|
"environmentVariables": [
|
|
38
54
|
{
|
|
39
|
-
"name": "
|
|
40
|
-
"description": "
|
|
41
|
-
"isRequired":
|
|
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,
|
|
42
58
|
"isSecret": true,
|
|
43
59
|
"format": "string"
|
|
44
60
|
},
|
|
45
61
|
{
|
|
46
|
-
"name": "
|
|
47
|
-
"description": "
|
|
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.",
|
|
48
64
|
"isRequired": false,
|
|
49
|
-
"isSecret":
|
|
65
|
+
"isSecret": false,
|
|
50
66
|
"format": "string"
|
|
51
67
|
}
|
|
52
68
|
]
|
|
@@ -54,23 +70,23 @@
|
|
|
54
70
|
{
|
|
55
71
|
"registryType": "pypi",
|
|
56
72
|
"identifier": "apiosk-mcp",
|
|
57
|
-
"version": "
|
|
73
|
+
"version": "2.0.0",
|
|
58
74
|
"transport": {
|
|
59
75
|
"type": "stdio"
|
|
60
76
|
},
|
|
61
77
|
"environmentVariables": [
|
|
62
78
|
{
|
|
63
|
-
"name": "
|
|
64
|
-
"description": "
|
|
65
|
-
"isRequired":
|
|
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,
|
|
66
82
|
"isSecret": true,
|
|
67
83
|
"format": "string"
|
|
68
84
|
},
|
|
69
85
|
{
|
|
70
|
-
"name": "
|
|
71
|
-
"description": "
|
|
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.",
|
|
72
88
|
"isRequired": false,
|
|
73
|
-
"isSecret":
|
|
89
|
+
"isSecret": false,
|
|
74
90
|
"format": "string"
|
|
75
91
|
}
|
|
76
92
|
]
|
|
@@ -84,8 +100,26 @@
|
|
|
84
100
|
],
|
|
85
101
|
"_meta": {
|
|
86
102
|
"com.apiosk": {
|
|
87
|
-
"tags": [
|
|
88
|
-
|
|
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
|
+
]
|
|
89
123
|
}
|
|
90
124
|
}
|
|
91
125
|
}
|
package/server.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import { registerBrandRoutes, BRAND_LINKS } from "./src/brand-routes.mjs";
|
|
1
2
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
5
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
5
6
|
import {
|
|
6
|
-
|
|
7
|
+
resolveServerPresentation,
|
|
7
8
|
createApioskMcpServer,
|
|
8
9
|
listApioskTools,
|
|
9
10
|
} from "./src/create-server.mjs";
|
|
11
|
+
import { V2_RESOURCE } from "./src/gateway-v2.mjs";
|
|
10
12
|
import {
|
|
11
13
|
createHostedOAuthSupport,
|
|
12
|
-
createMcpWalletAuthNonce,
|
|
13
14
|
resolveHostedMcpUrls,
|
|
14
15
|
} from "./src/oauth.mjs";
|
|
15
16
|
import { createApioskMcpRuntime } from "./src/runtime.mjs";
|
|
@@ -18,15 +19,14 @@ import {
|
|
|
18
19
|
resolveOpenAiAppsChallengeToken,
|
|
19
20
|
sendOpenAiAppsChallenge,
|
|
20
21
|
} from "./well-known.mjs";
|
|
21
|
-
import {
|
|
22
|
-
buildDiscoveryDocument,
|
|
23
|
-
getOpenApiRouteDocument,
|
|
24
|
-
} from "./src/publisher.mjs";
|
|
22
|
+
import { buildDiscoveryDocument } from "./src/well-known-routes.mjs";
|
|
25
23
|
import {
|
|
26
24
|
SETTLEMENT_DISCLOSURE_PATH,
|
|
27
25
|
createSettlementDisclosurePage,
|
|
28
26
|
} from "./src/settlement-disclosure.mjs";
|
|
29
27
|
|
|
28
|
+
const { info: SERVER_INFO, description: SERVER_DESCRIPTION, instructions: SERVER_INSTRUCTIONS } = resolveServerPresentation();
|
|
29
|
+
|
|
30
30
|
const CONTROL_PLANE_BACKEND_URL = (
|
|
31
31
|
process.env.APIOSK_CONTROL_PLANE_BACKEND_URL ||
|
|
32
32
|
process.env.APIOSK_DASHBOARD_URL ||
|
|
@@ -61,9 +61,9 @@ function normalizeControlPlanePath(pathname = "") {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function shouldProxyControlPlanePath(pathname = "") {
|
|
64
|
-
// Hosted sign-in
|
|
65
|
-
// /
|
|
66
|
-
//
|
|
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.
|
|
67
67
|
const normalizedPath = normalizeControlPlanePath(pathname);
|
|
68
68
|
return (
|
|
69
69
|
normalizedPath.startsWith("/api/credits/") ||
|
|
@@ -128,76 +128,33 @@ function renderMcpWelcomeHtml(mcpUrl) {
|
|
|
128
128
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
129
129
|
<meta name="robots" content="noindex" />
|
|
130
130
|
<title>Apiosk Connect</title>
|
|
131
|
+
${BRAND_LINKS}
|
|
131
132
|
<style>
|
|
132
|
-
:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.
|
|
142
|
-
|
|
143
|
-
color: #8aa0c6; border: 1px solid #2a3344; border-radius: 999px; padding: 4px 12px; margin-bottom: 20px;
|
|
144
|
-
}
|
|
145
|
-
h1 { font-size: 30px; margin: 0 0 12px; letter-spacing: -.02em; }
|
|
146
|
-
p { color: #b8c0cf; margin: 0 0 16px; }
|
|
147
|
-
.lead { color: #d6dce6; font-size: 17px; }
|
|
148
|
-
ul { color: #b8c0cf; margin: 0 0 16px; padding-left: 20px; }
|
|
149
|
-
li { margin: 6px 0; }
|
|
150
|
-
code {
|
|
151
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;
|
|
152
|
-
background: #141a24; border: 1px solid #232c3a; border-radius: 6px; padding: 2px 7px; color: #cfe0ff;
|
|
153
|
-
}
|
|
154
|
-
.card { background: #0f141c; border: 1px solid #1f2937; border-radius: 14px; padding: 28px 30px; }
|
|
155
|
-
.endpoint {
|
|
156
|
-
display: block; margin: 4px 0 20px; padding: 12px 14px; background: #141a24;
|
|
157
|
-
border: 1px solid #232c3a; border-radius: 8px; color: #cfe0ff; word-break: break-all;
|
|
158
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;
|
|
159
|
-
}
|
|
160
|
-
h2 { font-size: 13px; letter-spacing: .06em; text-transform: uppercase; color: #7f8aa0; margin: 24px 0 8px; }
|
|
161
|
-
a { color: #7aa2ff; text-decoration: none; }
|
|
162
|
-
a:hover { text-decoration: underline; }
|
|
163
|
-
.links { display: flex; gap: 18px; flex-wrap: wrap; margin-top: 18px; padding-top: 18px; border-top: 1px solid #1f2937; }
|
|
164
|
-
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}
|
|
165
144
|
</style>
|
|
166
145
|
</head>
|
|
167
146
|
<body>
|
|
168
147
|
<main>
|
|
169
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>
|
|
170
150
|
<span class="badge">Model Context Protocol</span>
|
|
171
|
-
<h1>
|
|
172
|
-
<p class="lead">
|
|
173
|
-
|
|
174
|
-
execute, and publish APIs through the Apiosk gateway. It is a machine endpoint, not a
|
|
175
|
-
website, so connect it from an MCP client (Claude, Cursor, ChatGPT, and others) rather
|
|
176
|
-
than browsing it here.
|
|
177
|
-
</p>
|
|
178
|
-
|
|
179
|
-
<h2>What you can do with it</h2>
|
|
180
|
-
<ul>
|
|
181
|
-
<li><strong>Discover</strong> APIs, datasets, and services in the Apiosk catalog.</li>
|
|
182
|
-
<li><strong>Pay</strong> per call automatically in USDC (x402 on Base).</li>
|
|
183
|
-
<li><strong>Execute</strong> any listing through a uniform contract or its API-specific tool.</li>
|
|
184
|
-
<li><strong>Publish & manage</strong> your own APIs so other agents can find and pay for them.</li>
|
|
185
|
-
<li><strong>Ship paid x402 routes from a coding agent</strong> — connect with
|
|
186
|
-
<code>Authorization: Bearer sk_live_…</code> (a provider API key) and use
|
|
187
|
-
<code>publish_x402_route</code>, <code>test_x402_route</code>, and friends.</li>
|
|
188
|
-
</ul>
|
|
189
|
-
|
|
190
|
-
<h2>Endpoint</h2>
|
|
191
|
-
<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>
|
|
192
154
|
|
|
193
|
-
<h2>
|
|
194
|
-
<code>claude mcp add --transport http apiosk ${mcpUrl}</code>
|
|
195
|
-
|
|
196
|
-
<h2>Connect from ChatGPT</h2>
|
|
155
|
+
<h2>Secure MCP endpoint</h2>
|
|
197
156
|
<code class="endpoint">${mcpUrl}</code>
|
|
198
|
-
|
|
199
|
-
<h2>Legacy SSE fallback</h2>
|
|
200
|
-
<code class="endpoint">${mcpUrl.replace(/\/mcp$/, "/sse")}</code>
|
|
157
|
+
<details><summary>Manual setup</summary><code>claude mcp add --transport http apiosk ${mcpUrl}</code><code>${mcpUrl.replace(/\/mcp$/, "/sse")}</code></details>
|
|
201
158
|
|
|
202
159
|
<div class="links">
|
|
203
160
|
<a href="/health">Health</a>
|
|
@@ -226,8 +183,7 @@ function sendMcpWelcome(req, res) {
|
|
|
226
183
|
res.status(200).json({
|
|
227
184
|
name: "Apiosk Connect",
|
|
228
185
|
server: SERVER_INFO,
|
|
229
|
-
description:
|
|
230
|
-
"Apiosk Connect lets MCP clients discover, pay for, execute, and publish APIs through the Apiosk gateway.",
|
|
186
|
+
description: SERVER_DESCRIPTION,
|
|
231
187
|
transport: "streamable-http",
|
|
232
188
|
endpoint: mcpUrl,
|
|
233
189
|
legacy_sse_endpoint: mcpUrl.replace(/\/mcp$/, "/sse"),
|
|
@@ -241,7 +197,8 @@ function sendMcpWelcome(req, res) {
|
|
|
241
197
|
|
|
242
198
|
// Public Fly deployment must accept the Fly hostname instead of localhost-only
|
|
243
199
|
// host validation defaults.
|
|
244
|
-
const
|
|
200
|
+
const bindHost = process.env.APIOSK_MCP_BIND_HOST || "0.0.0.0";
|
|
201
|
+
const app = createMcpExpressApp({ host: bindHost });
|
|
245
202
|
// Fly terminates TLS and adds one trusted proxy hop. Without this,
|
|
246
203
|
// express-rate-limit rejects X-Forwarded-For and can surface as MCP -32603.
|
|
247
204
|
app.set("trust proxy", 1);
|
|
@@ -251,10 +208,8 @@ const { issuerUrl, mcpServerUrl } = resolveHostedMcpUrls({
|
|
|
251
208
|
port,
|
|
252
209
|
});
|
|
253
210
|
const runtime = createApioskMcpRuntime({
|
|
254
|
-
// Hosted MCP is multi-tenant
|
|
255
|
-
//
|
|
256
|
-
// through OAuth/connect tokens; local wallets belong only to stdio installs.
|
|
257
|
-
enableLocalWallets: false,
|
|
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.
|
|
258
213
|
hostedAuthEnabled: true,
|
|
259
214
|
});
|
|
260
215
|
const hostedOAuth = createHostedOAuthSupport({
|
|
@@ -266,6 +221,24 @@ const hostedOAuth = createHostedOAuthSupport({
|
|
|
266
221
|
});
|
|
267
222
|
const mcpAuthMiddleware = hostedOAuth.createMcpAuthMiddleware(runtime);
|
|
268
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
|
+
|
|
269
242
|
app.use(hostedOAuth.metadataRouter);
|
|
270
243
|
app.use(new URL(hostedOAuth.oauthMetadata.authorization_endpoint).pathname, hostedOAuth.authorizationRouter);
|
|
271
244
|
app.use(new URL(hostedOAuth.oauthMetadata.token_endpoint).pathname, hostedOAuth.tokenRouter);
|
|
@@ -275,15 +248,55 @@ if (hostedOAuth.oauthMetadata.registration_endpoint) {
|
|
|
275
248
|
hostedOAuth.registrationRouter
|
|
276
249
|
);
|
|
277
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));
|
|
278
253
|
|
|
279
254
|
app.get(OPENAI_APPS_CHALLENGE_PATH_PATTERN, (req, res) => {
|
|
280
255
|
return sendOpenAiAppsChallenge(res, OPENAI_APPS_CHALLENGE_TOKEN);
|
|
281
256
|
});
|
|
282
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.
|
|
283
267
|
app.get("/logo-optimized-light.png", (req, res) => {
|
|
268
|
+
res.setHeader("cache-control", "public, max-age=0, must-revalidate");
|
|
284
269
|
res.type("png").sendFile(fileURLToPath(new URL("./logo-optimized-light.png", import.meta.url)));
|
|
285
270
|
});
|
|
286
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
|
+
|
|
287
300
|
app.get(SETTLEMENT_DISCLOSURE_PATH, (_req, res) => {
|
|
288
301
|
res
|
|
289
302
|
.setHeader("cache-control", "public, max-age=300")
|
|
@@ -291,55 +304,6 @@ app.get(SETTLEMENT_DISCLOSURE_PATH, (_req, res) => {
|
|
|
291
304
|
.send(createSettlementDisclosurePage());
|
|
292
305
|
});
|
|
293
306
|
|
|
294
|
-
// Self-hosted browser bundles for the /authorize wallet flows. Served
|
|
295
|
-
// same-origin because embedded mobile browsers can refuse cross-origin dynamic
|
|
296
|
-
// module imports. Regenerate with scripts/build-wallet-lib.mjs after upgrades.
|
|
297
|
-
const browserBundleFiles = new Map([
|
|
298
|
-
["wallet-accounts.mjs", new URL("./src/assets/wallet-accounts.mjs", import.meta.url)],
|
|
299
|
-
["walletconnect-provider.mjs", new URL("./src/assets/walletconnect-provider.mjs", import.meta.url)],
|
|
300
|
-
]);
|
|
301
|
-
const browserBundleCache = new Map();
|
|
302
|
-
|
|
303
|
-
app.get("/assets/:bundle", async (req, res, next) => {
|
|
304
|
-
const bundleUrl = browserBundleFiles.get(req.params.bundle);
|
|
305
|
-
if (!bundleUrl) return next();
|
|
306
|
-
|
|
307
|
-
try {
|
|
308
|
-
let bundle = browserBundleCache.get(req.params.bundle);
|
|
309
|
-
if (!bundle) {
|
|
310
|
-
const { readFile } = await import("node:fs/promises");
|
|
311
|
-
bundle = await readFile(bundleUrl);
|
|
312
|
-
browserBundleCache.set(req.params.bundle, bundle);
|
|
313
|
-
}
|
|
314
|
-
res.setHeader("content-type", "text/javascript; charset=utf-8");
|
|
315
|
-
res.setHeader("cache-control", "public, max-age=3600");
|
|
316
|
-
res.send(bundle);
|
|
317
|
-
} catch (error) {
|
|
318
|
-
res.status(404).json({
|
|
319
|
-
error: "not_found",
|
|
320
|
-
message: "Wallet library bundle is not available on this deployment.",
|
|
321
|
-
status: 404,
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
app.post("/api/auth/mcp-wallet-nonce", async (req, res) => {
|
|
327
|
-
try {
|
|
328
|
-
const nonce = await createMcpWalletAuthNonce({ env: process.env });
|
|
329
|
-
res.status(200).json(nonce);
|
|
330
|
-
} catch (error) {
|
|
331
|
-
const status = Number.isFinite(error?.status) ? error.status : 502;
|
|
332
|
-
res.status(status).json({
|
|
333
|
-
error: "wallet_auth_unavailable",
|
|
334
|
-
message:
|
|
335
|
-
error instanceof Error
|
|
336
|
-
? error.message
|
|
337
|
-
: "Wallet sign-in is temporarily unavailable.",
|
|
338
|
-
status,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
|
|
343
307
|
app.all("/api/*path", async (req, res) => {
|
|
344
308
|
try {
|
|
345
309
|
const pathname = req.path || req.originalUrl || "";
|
|
@@ -361,29 +325,50 @@ app.all("/api/*path", async (req, res) => {
|
|
|
361
325
|
}
|
|
362
326
|
});
|
|
363
327
|
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
// /
|
|
368
|
-
|
|
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) => {
|
|
369
338
|
try {
|
|
370
|
-
const
|
|
371
|
-
|
|
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],
|
|
372
367
|
});
|
|
373
|
-
if (!document) {
|
|
374
|
-
return res.status(404).json({
|
|
375
|
-
error: "not_found",
|
|
376
|
-
message: "No published route matches this id.",
|
|
377
|
-
status: 404,
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
res.setHeader("cache-control", "public, max-age=60");
|
|
381
|
-
res.json(document);
|
|
382
368
|
} catch (error) {
|
|
383
|
-
res.status(
|
|
384
|
-
error: "
|
|
369
|
+
res.status(500).json({
|
|
370
|
+
error: "server_card_unavailable",
|
|
385
371
|
message: error instanceof Error ? error.message : String(error),
|
|
386
|
-
status: 502,
|
|
387
372
|
});
|
|
388
373
|
}
|
|
389
374
|
});
|
|
@@ -422,7 +407,7 @@ app.get("/health", async (req, res) => {
|
|
|
422
407
|
});
|
|
423
408
|
|
|
424
409
|
app.post("/mcp", mcpAuthMiddleware, async (req, res) => {
|
|
425
|
-
const server = createApioskMcpServer({ runtime });
|
|
410
|
+
const server = createApioskMcpServer({ runtime, legacyUiMime: /openai|chatgpt/i.test(req.headers["user-agent"] || "") });
|
|
426
411
|
const transport = new StreamableHTTPServerTransport({
|
|
427
412
|
sessionIdGenerator: undefined,
|
|
428
413
|
});
|
|
@@ -555,11 +540,11 @@ app.post("/messages", mcpAuthMiddleware, async (req, res) => {
|
|
|
555
540
|
}
|
|
556
541
|
});
|
|
557
542
|
|
|
558
|
-
app.listen(port,
|
|
559
|
-
console.log(`Apiosk MCP server listening on http
|
|
560
|
-
console.log(`Health check: http
|
|
561
|
-
console.log(`MCP endpoint: http
|
|
562
|
-
console.log(`Legacy SSE endpoint: http
|
|
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`);
|
|
563
548
|
console.log(`OAuth issuer: ${issuerUrl.href}`);
|
|
564
549
|
console.log(`OAuth protected-resource metadata: ${hostedOAuth.resourceMetadataUrl}`);
|
|
565
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
|
+
}
|