@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,203 @@
1
+ // Apiosk external x402 payment (apiosk_fetch_paid).
2
+ //
3
+ // The last step of the agentic flow for endpoints the Apiosk gateway does NOT
4
+ // host: pay an arbitrary external x402 URL from the connected buyer's managed
5
+ // wallet. The MCP holds no signing keys, so this delegates to the gateway's
6
+ // gateway-as-payer proxy (POST /v1/x402/fetch): the gateway enforces the connect
7
+ // token's spend caps, pays the provider from a platform payer wallet, debits the
8
+ // buyer's managed wallet, and returns the provider response.
9
+ //
10
+ // For Apiosk CATALOG listings use apiosk_execute instead — that path is cheaper
11
+ // and fully settled. This tool is only for external (federated / discovered)
12
+ // endpoints. `confirmed_price_usdc` is required and is the in-chat price
13
+ // checkpoint: the gateway refuses if the live price exceeds what the user
14
+ // confirmed via apiosk_inspect_x402.
15
+
16
+ import { randomUUID } from "node:crypto";
17
+
18
+ const DEFAULT_GATEWAY_BASE_URL = "https://gateway.apiosk.com";
19
+
20
+ function content(value) {
21
+ const result = {
22
+ content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }],
23
+ };
24
+ if (value && typeof value === "object" && !Array.isArray(value)) {
25
+ result.structuredContent = value;
26
+ }
27
+ return result;
28
+ }
29
+
30
+ function trimString(value) {
31
+ return String(value ?? "").trim();
32
+ }
33
+
34
+ /**
35
+ * Execute a paid fetch of an external x402 endpoint via the gateway payer proxy.
36
+ *
37
+ * @param {object} args - { url, method?, query?, body?, headers?, confirmed_price_usdc, max_price_usdc?, idempotency_key? }
38
+ * @param {object} ctx - { connectToken, gatewayBaseUrl, fetchImpl? }
39
+ */
40
+ export async function runFetchPaid(args = {}, ctx = {}) {
41
+ const url = trimString(args.url);
42
+ if (!url) {
43
+ return content({ status: "error", error: "Missing required field: url" });
44
+ }
45
+ const confirmedPrice = args.confirmed_price_usdc;
46
+ if (!Number.isFinite(confirmedPrice) || confirmedPrice < 0) {
47
+ return content({
48
+ status: "error",
49
+ error:
50
+ "Missing required field: confirmed_price_usdc. Call apiosk_inspect_x402 on the URL first, tell the user the price, then pass the confirmed amount here.",
51
+ });
52
+ }
53
+
54
+ const connectToken = trimString(ctx.connectToken);
55
+ if (!connectToken) {
56
+ return content({
57
+ status: "error",
58
+ error:
59
+ "No connected wallet. apiosk_fetch_paid needs an Apiosk connect token (authorize the Apiosk app / OAuth on the hosted server). For Apiosk catalog listings, use apiosk_execute instead.",
60
+ });
61
+ }
62
+
63
+ const gatewayBaseUrl = trimString(ctx.gatewayBaseUrl) || DEFAULT_GATEWAY_BASE_URL;
64
+ const fetchImpl = ctx.fetchImpl || fetch;
65
+ const idempotencyKey = trimString(args.idempotency_key) || randomUUID();
66
+ const method = trimString(args.method).toUpperCase() || "GET";
67
+
68
+ const requestBody = {
69
+ url,
70
+ method,
71
+ query: args.query && typeof args.query === "object" ? args.query : undefined,
72
+ body: args.body !== undefined ? args.body : undefined,
73
+ headers: args.headers && typeof args.headers === "object" ? args.headers : undefined,
74
+ confirmed_price_usdc: confirmedPrice,
75
+ max_price_usdc: Number.isFinite(args.max_price_usdc) ? args.max_price_usdc : undefined,
76
+ };
77
+
78
+ let response;
79
+ try {
80
+ response = await fetchImpl(`${gatewayBaseUrl.replace(/\/+$/, "")}/v1/x402/fetch`, {
81
+ method: "POST",
82
+ cache: "no-store",
83
+ headers: {
84
+ "content-type": "application/json",
85
+ accept: "application/json",
86
+ "X-Apiosk-Connect-Token": connectToken,
87
+ "Idempotency-Key": idempotencyKey,
88
+ },
89
+ body: JSON.stringify(requestBody),
90
+ });
91
+ } catch (error) {
92
+ return content({
93
+ status: "error",
94
+ error: `Could not reach the Apiosk gateway payer: ${trimString(error?.message || error)}`,
95
+ idempotency_key: idempotencyKey,
96
+ });
97
+ }
98
+
99
+ const text = await response.text().catch(() => "");
100
+ let payload = null;
101
+ try {
102
+ payload = text ? JSON.parse(text) : null;
103
+ } catch {
104
+ payload = text || null;
105
+ }
106
+
107
+ // The gateway emits refusal codes under `error` (json_error_response) or
108
+ // `code`; normalize so callers below can read either.
109
+ const gatewayCode = payload?.code || payload?.error || null;
110
+
111
+ // Feature not enabled yet: degrade gracefully so the model falls back to a
112
+ // catalog listing instead of surfacing a raw 403.
113
+ if (response.status === 403 && gatewayCode === "feature_disabled") {
114
+ return content({
115
+ status: "unavailable",
116
+ code: "feature_disabled",
117
+ message:
118
+ "External direct-pay (apiosk_fetch_paid) is not enabled on this gateway yet. Use an Apiosk catalog listing via apiosk_execute instead.",
119
+ idempotency_key: idempotencyKey,
120
+ });
121
+ }
122
+
123
+ // Any non-2xx with a structured code is a business outcome (price too high,
124
+ // host not allowed, wallet over cap) — return it as data, not an MCP error, so
125
+ // the model can adapt (lower the amount, pick another endpoint, tell the user).
126
+ if (!response.ok) {
127
+ return content({
128
+ status: payload?.status || (response.status === 402 ? "payment_required" : "refused"),
129
+ code: gatewayCode || `http_${response.status}`,
130
+ message: payload?.message || payload?.error || `Gateway returned HTTP ${response.status}.`,
131
+ http_status: response.status,
132
+ idempotency_key: idempotencyKey,
133
+ });
134
+ }
135
+
136
+ // Success: surface the provider data + a receipt the model can quote back.
137
+ return content({
138
+ status: "success",
139
+ data: payload?.data ?? payload,
140
+ receipt: payload?.receipt || null,
141
+ idempotency_key: idempotencyKey,
142
+ });
143
+ }
144
+
145
+ export const FETCH_PAID_TOOL = {
146
+ name: "apiosk_fetch_paid",
147
+ description:
148
+ "Pay an EXTERNAL x402 endpoint (one Apiosk does not host) from the connected wallet and return its data. Use this only for external results from apiosk_discover (executable_via='apiosk_fetch_paid'); for Apiosk catalog listings use apiosk_execute. REQUIRED: call apiosk_inspect_x402 on the url first, tell the user the exact price, and pass that amount as confirmed_price_usdc — the gateway refuses if the live price is higher. The gateway enforces the wallet's per-tx/daily spend limits. Base + USDC only.",
149
+ annotations: {
150
+ readOnlyHint: false,
151
+ destructiveHint: true,
152
+ idempotentHint: false,
153
+ openWorldHint: true,
154
+ },
155
+ _meta: {
156
+ "openai/outputTemplate": "ui://apiosk/result-canvas.html",
157
+ "openai/toolInvocation/invoking": "Paying the provider and fetching data…",
158
+ "openai/toolInvocation/invoked": "Paid data received",
159
+ ui: { resourceUri: "ui://apiosk/result-canvas.html" },
160
+ },
161
+ inputSchema: {
162
+ type: "object",
163
+ required: ["url", "confirmed_price_usdc"],
164
+ properties: {
165
+ url: {
166
+ type: "string",
167
+ description: "The external x402 resource URL to pay and fetch (from a discovery result's `url`).",
168
+ },
169
+ confirmed_price_usdc: {
170
+ type: "number",
171
+ description: "The price you read via apiosk_inspect_x402 and confirmed with the user. The gateway refuses if the live price exceeds this.",
172
+ },
173
+ method: {
174
+ type: "string",
175
+ enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
176
+ description: "HTTP method for the provider request. Defaults to GET.",
177
+ },
178
+ query: {
179
+ type: "object",
180
+ additionalProperties: true,
181
+ description: "Optional query parameters to send to the provider.",
182
+ },
183
+ body: {
184
+ type: "object",
185
+ additionalProperties: true,
186
+ description: "Optional JSON request body for POST/PUT/PATCH.",
187
+ },
188
+ headers: {
189
+ type: "object",
190
+ additionalProperties: true,
191
+ description: "Optional extra request headers (allowlisted server-side; secrets are never accepted here).",
192
+ },
193
+ max_price_usdc: {
194
+ type: "number",
195
+ description: "Optional additional per-call ceiling. The gateway also enforces the wallet's per-tx and daily limits.",
196
+ },
197
+ idempotency_key: {
198
+ type: "string",
199
+ description: "Optional. Reuse the same key to safely retry without paying twice; a fresh one is generated if omitted.",
200
+ },
201
+ },
202
+ },
203
+ };