@apiosk/mcp 1.3.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -9
- package/docs/sepa-rail.md +13 -13
- package/dxt.json +4 -4
- package/logo-optimized-light.png +0 -0
- package/package.json +6 -4
- package/server.json +58 -3
- package/server.mjs +213 -11
- package/src/assets/wallet-accounts.mjs +20513 -0
- package/src/assets/walletconnect-provider.mjs +6319 -0
- package/src/create-server.mjs +47 -2
- package/src/discovery.mjs +929 -0
- package/src/external-fetch.mjs +203 -0
- package/src/hosted-payment.mjs +552 -0
- package/src/hosted-wallets.mjs +530 -0
- package/src/oauth.mjs +2011 -225
- package/src/observability.mjs +194 -0
- package/src/payment-guidance.mjs +15 -24
- package/src/publisher.mjs +1288 -0
- package/src/result-canvas.mjs +16 -0
- package/src/runtime.mjs +609 -30
- package/src/settlement-disclosure.mjs +26 -0
- package/src/source-registry.mjs +215 -0
- package/src/x402-inspect.mjs +361 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// MCP observability side-car — the MCP server historically logged NOTHING about
|
|
2
|
+
// its own activity (tool calls, SSE sessions, OAuth/installs). This module writes
|
|
3
|
+
// append-only rows to the mcp_tool_calls / mcp_sessions / mcp_oauth_events tables
|
|
4
|
+
// (gateway migration 057) so the admin portal can see MCP traffic.
|
|
5
|
+
//
|
|
6
|
+
// Design: fire-and-forget, NEVER throws into the hot path (every write is wrapped
|
|
7
|
+
// in try/catch and returns a swallowed promise). Uses the service-role key exactly
|
|
8
|
+
// like publisher.mjs (rest/v1/<table>). PRIVACY: raw connect tokens are sha256-hashed
|
|
9
|
+
// (never stored raw), and only argument KEY NAMES are stored — never values, headers,
|
|
10
|
+
// or bodies.
|
|
11
|
+
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_SUPABASE_URL = "https://jgjoiyqdyypouskftzeq.supabase.co";
|
|
15
|
+
|
|
16
|
+
function resolveConfig(env = {}) {
|
|
17
|
+
const raw =
|
|
18
|
+
env.APIOSK_SUPABASE_URL || env.SUPABASE_URL || DEFAULT_SUPABASE_URL;
|
|
19
|
+
const url = String(raw).replace(/\/+$/, "");
|
|
20
|
+
const key =
|
|
21
|
+
env.APIOSK_SUPABASE_SERVICE_ROLE_KEY || env.SUPABASE_SERVICE_ROLE_KEY || "";
|
|
22
|
+
return { url, key };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sha256(value) {
|
|
26
|
+
if (!value || typeof value !== "string") return null;
|
|
27
|
+
try {
|
|
28
|
+
return createHash("sha256").update(value).digest("hex");
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function trimStr(v, max = 400) {
|
|
35
|
+
if (v == null) return null;
|
|
36
|
+
const s = String(v);
|
|
37
|
+
return s.length > max ? s.slice(0, max) : s;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Low-level fire-and-forget REST write. Resolves regardless of outcome; a failure
|
|
41
|
+
// is logged to stderr but never propagates — observability must not break a tool call.
|
|
42
|
+
async function restWrite(env, path, body, { method = "POST", extraHeaders = {} } = {}) {
|
|
43
|
+
const { url, key } = resolveConfig(env);
|
|
44
|
+
if (!key) return; // not configured on this deployment — skip silently
|
|
45
|
+
const fetchImpl = globalThis.fetch;
|
|
46
|
+
if (typeof fetchImpl !== "function") return;
|
|
47
|
+
try {
|
|
48
|
+
await fetchImpl(`${url}/rest/v1/${path}`, {
|
|
49
|
+
method,
|
|
50
|
+
headers: {
|
|
51
|
+
apikey: key,
|
|
52
|
+
authorization: `Bearer ${key}`,
|
|
53
|
+
"content-type": "application/json",
|
|
54
|
+
prefer: "return=minimal",
|
|
55
|
+
...extraHeaders,
|
|
56
|
+
},
|
|
57
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
try {
|
|
61
|
+
console.warn(
|
|
62
|
+
"[observability] write failed:",
|
|
63
|
+
error && error.message ? error.message : String(error),
|
|
64
|
+
);
|
|
65
|
+
} catch {
|
|
66
|
+
/* ignore */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Extract caller identity from the MCP authInfo (never returns a raw token). */
|
|
72
|
+
export function callerFrom(authInfo) {
|
|
73
|
+
const x = (authInfo && (authInfo.extra || authInfo)) || {};
|
|
74
|
+
const rawToken = x.apiosk_connect_token || x.connectToken || null;
|
|
75
|
+
const hasToken = Boolean(rawToken || x.apiosk_connect_token_id);
|
|
76
|
+
return {
|
|
77
|
+
auth_method: hasToken ? "connect_token" : x.userId ? "oauth" : "anonymous",
|
|
78
|
+
user_id: x.userId || x.user_id || null,
|
|
79
|
+
connect_token_id: x.apiosk_connect_token_id || x.connect_token_id || null,
|
|
80
|
+
connect_token_hash: sha256(rawToken),
|
|
81
|
+
wallet_address:
|
|
82
|
+
x.walletAddress || x.apiosk_connect_wallet_address || x.wallet_address || null,
|
|
83
|
+
provider_id: x.providerId || x.provider_id || null,
|
|
84
|
+
client_name: (authInfo && (authInfo.clientName || authInfo.client_name)) || null,
|
|
85
|
+
client_kind: (authInfo && (authInfo.clientKind || authInfo.client_kind)) || null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Log one tools/call dispatch. Fire-and-forget. */
|
|
90
|
+
export function logToolCall(
|
|
91
|
+
env,
|
|
92
|
+
{
|
|
93
|
+
toolName,
|
|
94
|
+
outcome = "ok",
|
|
95
|
+
errorCode = null,
|
|
96
|
+
latencyMs = null,
|
|
97
|
+
authInfo = null,
|
|
98
|
+
argKeys = [],
|
|
99
|
+
sessionId = null,
|
|
100
|
+
gatewayRequestId = null,
|
|
101
|
+
ip = null,
|
|
102
|
+
userAgent = null,
|
|
103
|
+
} = {},
|
|
104
|
+
) {
|
|
105
|
+
const caller = callerFrom(authInfo);
|
|
106
|
+
return restWrite(env, "mcp_tool_calls", {
|
|
107
|
+
tool_name: String(toolName || "unknown"),
|
|
108
|
+
outcome,
|
|
109
|
+
error_code: errorCode ? trimStr(errorCode, 120) : null,
|
|
110
|
+
latency_ms: typeof latencyMs === "number" ? Math.round(latencyMs) : null,
|
|
111
|
+
...caller,
|
|
112
|
+
session_id: sessionId,
|
|
113
|
+
gateway_request_id: gatewayRequestId,
|
|
114
|
+
arg_keys: Array.isArray(argKeys) ? argKeys.slice(0, 64).map(String) : [],
|
|
115
|
+
ip_address: ip,
|
|
116
|
+
user_agent: trimStr(userAgent),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Record an SSE session on connect (upsert on session_id). Fire-and-forget. */
|
|
121
|
+
export function openSession(
|
|
122
|
+
env,
|
|
123
|
+
{ sessionId, transport = "sse", ip = null, userAgent = null, clientName = null, clientKind = null, protocolVersion = null } = {},
|
|
124
|
+
) {
|
|
125
|
+
if (!sessionId) return;
|
|
126
|
+
return restWrite(
|
|
127
|
+
env,
|
|
128
|
+
"mcp_sessions",
|
|
129
|
+
{
|
|
130
|
+
session_id: String(sessionId),
|
|
131
|
+
transport,
|
|
132
|
+
ip_address: ip,
|
|
133
|
+
user_agent: trimStr(userAgent),
|
|
134
|
+
client_name: clientName,
|
|
135
|
+
client_kind: clientKind,
|
|
136
|
+
protocol_version: protocolVersion,
|
|
137
|
+
status: "online",
|
|
138
|
+
},
|
|
139
|
+
// Upsert: a reconnect with the same id refreshes it instead of 409-ing.
|
|
140
|
+
{ extraHeaders: { prefer: "resolution=merge-duplicates,return=minimal" } },
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Mark an SSE session closed on disconnect. Fire-and-forget. */
|
|
145
|
+
export function closeSession(env, sessionId) {
|
|
146
|
+
if (!sessionId) return;
|
|
147
|
+
const now = new Date().toISOString();
|
|
148
|
+
return restWrite(
|
|
149
|
+
env,
|
|
150
|
+
`mcp_sessions?session_id=eq.${encodeURIComponent(String(sessionId))}`,
|
|
151
|
+
{ status: "closed", disconnected_at: now, last_activity_at: now },
|
|
152
|
+
{ method: "PATCH" },
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Log an OAuth / install event (authorize, consent, token_issued, wallet_created…). */
|
|
157
|
+
export function logOAuthEvent(
|
|
158
|
+
env,
|
|
159
|
+
{
|
|
160
|
+
eventType,
|
|
161
|
+
userId = null,
|
|
162
|
+
clientId = null,
|
|
163
|
+
clientName = null,
|
|
164
|
+
redirectUri = null,
|
|
165
|
+
scopes = [],
|
|
166
|
+
connectTokenId = null,
|
|
167
|
+
connectTokenHash = null,
|
|
168
|
+
connectTokenRaw = null,
|
|
169
|
+
walletAddress = null,
|
|
170
|
+
walletCreated = false,
|
|
171
|
+
outcome = "ok",
|
|
172
|
+
errorCode = null,
|
|
173
|
+
ip = null,
|
|
174
|
+
userAgent = null,
|
|
175
|
+
} = {},
|
|
176
|
+
) {
|
|
177
|
+
if (!eventType) return;
|
|
178
|
+
return restWrite(env, "mcp_oauth_events", {
|
|
179
|
+
event_type: eventType,
|
|
180
|
+
user_id: userId,
|
|
181
|
+
client_id: clientId ? trimStr(clientId, 200) : null,
|
|
182
|
+
client_name: clientName,
|
|
183
|
+
redirect_uri: redirectUri ? trimStr(redirectUri, 500) : null,
|
|
184
|
+
scopes: Array.isArray(scopes) ? scopes.map(String) : [],
|
|
185
|
+
connect_token_id: connectTokenId,
|
|
186
|
+
connect_token_hash: connectTokenHash || sha256(connectTokenRaw),
|
|
187
|
+
wallet_address: walletAddress,
|
|
188
|
+
wallet_created: Boolean(walletCreated),
|
|
189
|
+
outcome,
|
|
190
|
+
error_code: errorCode ? trimStr(errorCode, 120) : null,
|
|
191
|
+
ip_address: ip,
|
|
192
|
+
user_agent: trimStr(userAgent),
|
|
193
|
+
});
|
|
194
|
+
}
|
package/src/payment-guidance.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// This module turns the Apiosk settlement model into agent-readable guidance
|
|
4
4
|
// that is surfaced at discovery time (search/explore/get_api) and through the
|
|
5
5
|
// dedicated apiosk_payment_guide tool. It covers BOTH sides of the gateway:
|
|
6
|
-
// - buyers: how an agent pays for a paid API call (USDC x402
|
|
6
|
+
// - buyers: how an agent pays for a paid API call (USDC over x402),
|
|
7
7
|
// tailored to what auth the runtime currently has.
|
|
8
8
|
// - providers (sellers): how to publish an API so other agents can pay for it.
|
|
9
9
|
//
|
|
@@ -23,21 +23,12 @@ export const SETTLEMENT_RAILS = [
|
|
|
23
23
|
best_for: "Autonomous agents that hold a funded Base USDC wallet.",
|
|
24
24
|
setup: "Fund a wallet with Base mainnet USDC, then settlement happens automatically per call.",
|
|
25
25
|
},
|
|
26
|
-
{
|
|
27
|
-
id: "credits",
|
|
28
|
-
label: "Prepaid credits",
|
|
29
|
-
summary:
|
|
30
|
-
"A human tops up a credits balance once and the agent spends it down per call.",
|
|
31
|
-
best_for: "Letting a human fund usage once and then handing the agent autonomy.",
|
|
32
|
-
setup: "Top up credits via the Apiosk buyer portal; the agent spends them down automatically.",
|
|
33
|
-
},
|
|
34
26
|
];
|
|
35
27
|
|
|
36
28
|
// The order the gateway tries to cover a paid call. A 402 is only returned
|
|
37
29
|
// when none of the buyer's enabled rails can settle.
|
|
38
30
|
export const RAIL_FALLBACK_ORDER = [
|
|
39
31
|
"1. USDC / x402 wallet when the agent can produce a payment proof.",
|
|
40
|
-
"2. Prepaid credits balance.",
|
|
41
32
|
];
|
|
42
33
|
|
|
43
34
|
function resolvePrice(api) {
|
|
@@ -83,7 +74,7 @@ function describeReadiness(capability = {}, { localWalletsEnabled = false, mode
|
|
|
83
74
|
status: "ready_to_pay",
|
|
84
75
|
active_method: "Apiosk connect token",
|
|
85
76
|
detail:
|
|
86
|
-
"A managed connect token is active. The gateway settles each call
|
|
77
|
+
"A managed connect token is active. The gateway settles each call from the authorized USDC managed wallet server-side, no signing needed here.",
|
|
87
78
|
};
|
|
88
79
|
case "wallet_address":
|
|
89
80
|
return {
|
|
@@ -91,7 +82,7 @@ function describeReadiness(capability = {}, { localWalletsEnabled = false, mode
|
|
|
91
82
|
status: "setup_required",
|
|
92
83
|
active_method: "wallet address only",
|
|
93
84
|
detail:
|
|
94
|
-
"A wallet address is known but no signing key or connect token is configured, so this surface cannot settle x402 calls itself.
|
|
85
|
+
"A wallet address is known but no signing key or connect token is configured, so this surface cannot settle x402 calls itself.",
|
|
95
86
|
};
|
|
96
87
|
default:
|
|
97
88
|
return {
|
|
@@ -117,24 +108,24 @@ function buildHowToPaySteps({ readiness, localWalletsEnabled, mode, slug }) {
|
|
|
117
108
|
if (readiness.ready) {
|
|
118
109
|
return [
|
|
119
110
|
execHint,
|
|
120
|
-
"Settlement is automatic
|
|
121
|
-
"If a call still returns payment_required, fund the wallet
|
|
111
|
+
"Settlement is automatic, the gateway charges the active method and returns the result.",
|
|
112
|
+
"If a call still returns payment_required, fund the wallet with USDC on Base and retry.",
|
|
122
113
|
];
|
|
123
114
|
}
|
|
124
115
|
|
|
125
116
|
if (localWalletsEnabled) {
|
|
126
117
|
return [
|
|
127
118
|
"Run apiosk_get_started to create or select a local wallet (or import a dashboard connect string).",
|
|
128
|
-
"Fund the wallet with Base mainnet USDC using apiosk_show_wallet_funding
|
|
119
|
+
"Fund the wallet with Base mainnet USDC using apiosk_show_wallet_funding.",
|
|
129
120
|
execHint,
|
|
130
121
|
];
|
|
131
122
|
}
|
|
132
123
|
|
|
133
124
|
if (mode === "hosted") {
|
|
134
125
|
return [
|
|
135
|
-
"Authorize the Apiosk app when your MCP client prompts, so calls settle against your managed wallet
|
|
126
|
+
"Authorize the Apiosk app when your MCP client prompts, so calls settle against your managed wallet.",
|
|
136
127
|
execHint,
|
|
137
|
-
"If a call returns payment_required,
|
|
128
|
+
"If a call returns payment_required, fund your managed wallet with USDC on Base, then retry.",
|
|
138
129
|
];
|
|
139
130
|
}
|
|
140
131
|
|
|
@@ -163,7 +154,7 @@ export function buildPaymentGuidance({
|
|
|
163
154
|
if (isFree) {
|
|
164
155
|
return {
|
|
165
156
|
role: "buyer",
|
|
166
|
-
summary: `This listing is free (cost_per_call ${price}). Call it directly
|
|
157
|
+
summary: `This listing is free (cost_per_call ${price}). Call it directly, no payment required.`,
|
|
167
158
|
cost_per_call_usd: price,
|
|
168
159
|
free: true,
|
|
169
160
|
status: "ready_to_pay",
|
|
@@ -192,7 +183,7 @@ export function buildPaymentGuidance({
|
|
|
192
183
|
settlement_rails: SETTLEMENT_RAILS,
|
|
193
184
|
rail_fallback_order: RAIL_FALLBACK_ORDER,
|
|
194
185
|
on_payment_required:
|
|
195
|
-
"A paid call can return a structured payment_required error when
|
|
186
|
+
"A paid call can return a structured payment_required error when the wallet cannot cover it. Fund the wallet with USDC on Base, then retry the same call.",
|
|
196
187
|
base_chain: { chain_id: BASE_CHAIN_ID, usdc_contract: BASE_USDC_CONTRACT, network: "base" },
|
|
197
188
|
learn_more: "Call apiosk_help with topic='rails' for the full settlement model.",
|
|
198
189
|
};
|
|
@@ -227,7 +218,7 @@ export function buildProviderGuidance({ mode = "remote", localWalletsEnabled = f
|
|
|
227
218
|
"Host your MCP over HTTPS and support initialize, tools/list, and tools/call.",
|
|
228
219
|
"Protect the upstream MCP with bearer auth or another server-side secret; Apiosk injects that credential after payment.",
|
|
229
220
|
"Import the MCP in the provider portal. Apiosk scans tools/list and creates one paid action per selected tool.",
|
|
230
|
-
"Buyers should use https://mcp.apiosk.com/mcp, GET https://gateway.apiosk.com/<slug>/metadata, or POST https://gateway.apiosk.com/<slug>/execute
|
|
221
|
+
"Buyers should use https://mcp.apiosk.com/mcp, GET https://gateway.apiosk.com/<slug>/metadata, or POST https://gateway.apiosk.com/<slug>/execute, not your raw MCP URL.",
|
|
231
222
|
"Your MCP should not return 402 or inspect X-Payment. Apiosk handles payment challenges, settlement, and revenue splits before calling your MCP.",
|
|
232
223
|
],
|
|
233
224
|
requirements: [
|
|
@@ -266,17 +257,17 @@ export function buildPaymentGuide({
|
|
|
266
257
|
const payload = {
|
|
267
258
|
role: normalizedRole,
|
|
268
259
|
overview:
|
|
269
|
-
"Apiosk is one gateway, any rail: a single buyer identity can pay for any API over USDC (x402 on Base) or prepaid credits
|
|
260
|
+
"Apiosk is one gateway, any rail: a single buyer identity can pay for any API over USDC (x402 on Base) or prepaid credits, the gateway picks the rail per call. Providers list APIs once and get paid per call.",
|
|
270
261
|
quickstart: {
|
|
271
262
|
buyer: [
|
|
272
263
|
"Discover: apiosk_search or apiosk_explore.",
|
|
273
264
|
"Inspect: apiosk_get_api for price, schema, and a per-listing payment block.",
|
|
274
|
-
"Pay & run: call the dynamic tool or apiosk_execute
|
|
265
|
+
"Pay & run: call the dynamic tool or apiosk_execute, settlement is automatic once a rail is configured.",
|
|
275
266
|
],
|
|
276
267
|
provider: [
|
|
277
268
|
"Get a signing wallet (apiosk_wallet_create or APIOSK_PRIVATE_KEY).",
|
|
278
269
|
"Publish: apiosk_publish_api with name, slug, https endpoint_url, price_usd, description.",
|
|
279
|
-
"Confirm: apiosk_list_my_apis
|
|
270
|
+
"Confirm: apiosk_list_my_apis, your listing is now discoverable and payable.",
|
|
280
271
|
],
|
|
281
272
|
},
|
|
282
273
|
};
|
|
@@ -299,7 +290,7 @@ export function buildDiscoveryPaymentHint({ capability = {}, mode = "remote", lo
|
|
|
299
290
|
payment_ready: readiness.ready,
|
|
300
291
|
active_method: readiness.active_method,
|
|
301
292
|
how_to_pay: readiness.ready
|
|
302
|
-
? "Paid calls settle automatically
|
|
293
|
+
? "Paid calls settle automatically, just call the tool or apiosk_execute."
|
|
303
294
|
: readiness.detail,
|
|
304
295
|
settlement_rails: SETTLEMENT_RAILS.map((rail) => rail.id),
|
|
305
296
|
learn_more: "Call apiosk_payment_guide for full buyer + provider instructions.",
|