@apiosk/mcp 1.3.0 → 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.
@@ -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
+ }
@@ -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 or prepaid credits),
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 over the buyer's enabled rails (USDC managed wallet or credits) server-side — no signing needed here.",
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. The gateway may still settle over credits if a balance exists for this buyer.",
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 — the gateway charges the active method and returns the result.",
121
- "If a call still returns payment_required, fund the wallet (apiosk_show_wallet_funding) or enable another rail, then retry.",
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, or top up credits for managed settlement.",
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/rails.",
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, top up credits via the buyer portal or fund your managed wallet, then retry.",
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 — no payment required.`,
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 no enabled rail can cover it. Fund the wallet or top up credits, then retry the same call.",
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 — not your raw MCP URL.",
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 — the gateway picks the rail per call. Providers list APIs once and get paid per call.",
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 — settlement is automatic once a rail is configured.",
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 — your listing is now discoverable and payable.",
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 — just call the tool or apiosk_execute."
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.",