@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/src/funding-options.mjs
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { createHmac } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
import QRCode from "qrcode";
|
|
4
|
-
|
|
5
|
-
const TRANSAK_API_URL = "https://api-gateway.transak.com";
|
|
6
|
-
const ONRAMPER_WIDGET_URL = "https://buy.onramper.com";
|
|
7
|
-
const BASE_EXPLORER_URL = "https://basescan.org";
|
|
8
|
-
|
|
9
|
-
function trimString(value) {
|
|
10
|
-
return String(value || "").trim();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function boolFromEnv(value, defaultValue = true) {
|
|
14
|
-
const normalized = trimString(value).toLowerCase();
|
|
15
|
-
if (!normalized) return defaultValue;
|
|
16
|
-
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
17
|
-
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
18
|
-
return defaultValue;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function qrImageUrl(value) {
|
|
22
|
-
return `https://api.qrserver.com/v1/create-qr-code/?size=280x280&data=${encodeURIComponent(value)}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function getOnramperConfig(env = process.env) {
|
|
26
|
-
return {
|
|
27
|
-
apiKey: trimString(env.ONRAMPER_API_KEY),
|
|
28
|
-
widgetSecret: trimString(env.ONRAMPER_WIDGET_SECRET),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function getTransakConfig(env = process.env) {
|
|
33
|
-
return {
|
|
34
|
-
apiKey: trimString(env.TRANSAK_API_KEY),
|
|
35
|
-
secret: trimString(env.TRANSAK_SECRET),
|
|
36
|
-
referrerDomain: trimString(env.APIOSK_TRANSAK_REFERRER_DOMAIN || "dashboard.apiosk.com"),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function buildOnramperWidgetUrl(address, env = process.env) {
|
|
41
|
-
const { apiKey, widgetSecret } = getOnramperConfig(env);
|
|
42
|
-
const normalizedAddress = trimString(address).toLowerCase();
|
|
43
|
-
const searchParams = new URLSearchParams({
|
|
44
|
-
apiKey,
|
|
45
|
-
mode: "buy",
|
|
46
|
-
defaultCrypto: "USDC",
|
|
47
|
-
defaultNetwork: "base",
|
|
48
|
-
onlyCryptoNetworks: "base",
|
|
49
|
-
networkWallets: `base:${normalizedAddress}`,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const signContent = `networkWallets=base:${normalizedAddress}`;
|
|
53
|
-
const signature = createHmac("sha256", widgetSecret)
|
|
54
|
-
.update(signContent)
|
|
55
|
-
.digest("hex");
|
|
56
|
-
|
|
57
|
-
searchParams.set("signature", signature);
|
|
58
|
-
|
|
59
|
-
return `${ONRAMPER_WIDGET_URL}/?${searchParams.toString()}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function getTransakAccessToken(env = process.env, fetchImpl = globalThis.fetch) {
|
|
63
|
-
const { apiKey, secret } = getTransakConfig(env);
|
|
64
|
-
const response = await fetchImpl(`${TRANSAK_API_URL}/api/v2/auth/token`, {
|
|
65
|
-
method: "POST",
|
|
66
|
-
headers: { "Content-Type": "application/json" },
|
|
67
|
-
body: JSON.stringify({
|
|
68
|
-
apiKey,
|
|
69
|
-
secretKey: secret,
|
|
70
|
-
}),
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
throw new Error("Failed to get Transak access token.");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const data = await response.json();
|
|
78
|
-
return String(data.accessToken || "");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function createTransakFundingSession(address, env = process.env, fetchImpl = globalThis.fetch) {
|
|
82
|
-
const { apiKey, referrerDomain } = getTransakConfig(env);
|
|
83
|
-
const accessToken = await getTransakAccessToken(env, fetchImpl);
|
|
84
|
-
|
|
85
|
-
const sessionResponse = await fetchImpl(`${TRANSAK_API_URL}/api/v2/auth/session`, {
|
|
86
|
-
method: "POST",
|
|
87
|
-
headers: {
|
|
88
|
-
"Content-Type": "application/json",
|
|
89
|
-
"access-token": accessToken,
|
|
90
|
-
},
|
|
91
|
-
body: JSON.stringify({
|
|
92
|
-
widgetParams: {
|
|
93
|
-
apiKey,
|
|
94
|
-
referrerDomain,
|
|
95
|
-
walletAddress: address,
|
|
96
|
-
cryptoCurrencyCode: "USDC",
|
|
97
|
-
network: "base",
|
|
98
|
-
defaultCryptoCurrency: "USDC",
|
|
99
|
-
defaultNetwork: "base",
|
|
100
|
-
disableWalletAddressForm: true,
|
|
101
|
-
themeColor: "0052FF",
|
|
102
|
-
hideMenu: true,
|
|
103
|
-
},
|
|
104
|
-
}),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
if (!sessionResponse.ok) {
|
|
108
|
-
throw new Error("Failed to create Transak funding session.");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const sessionData = await sessionResponse.json();
|
|
112
|
-
return {
|
|
113
|
-
widget_url: sessionData.widgetUrl || null,
|
|
114
|
-
session_id: sessionData.sessionId || null,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function buildQrBundle(address, options = {}) {
|
|
119
|
-
const env = options.env || process.env;
|
|
120
|
-
const includeDataUrl =
|
|
121
|
-
options.includeQrDataUrl === true || boolFromEnv(env.APIOSK_INCLUDE_QR_DATA_URL, false);
|
|
122
|
-
const qrEnabled = boolFromEnv(env.APIOSK_ENABLE_QR, true);
|
|
123
|
-
const transferUri = `ethereum:${address}@8453`;
|
|
124
|
-
const payload = address;
|
|
125
|
-
const bundle = {
|
|
126
|
-
qr_payload: payload,
|
|
127
|
-
transfer_uri: transferUri,
|
|
128
|
-
qr_image_url: qrImageUrl(payload),
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
if (!qrEnabled) {
|
|
132
|
-
return bundle;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
bundle.qr_code_terminal = await QRCode.toString(payload, {
|
|
137
|
-
type: "terminal",
|
|
138
|
-
small: true,
|
|
139
|
-
});
|
|
140
|
-
bundle.qr_code_terminal_format = "ansi";
|
|
141
|
-
} catch {
|
|
142
|
-
// Leave terminal QR unset if rendering fails.
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (includeDataUrl) {
|
|
146
|
-
try {
|
|
147
|
-
bundle.qr_code_data_url = await QRCode.toDataURL(payload, {
|
|
148
|
-
margin: 1,
|
|
149
|
-
width: 280,
|
|
150
|
-
});
|
|
151
|
-
} catch {
|
|
152
|
-
// Leave data URL unset if rendering fails.
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return bundle;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function buildFundingProviders(address, env = process.env) {
|
|
160
|
-
const { apiKey: transakApiKey, secret: transakSecret } = getTransakConfig(env);
|
|
161
|
-
const { apiKey: onramperApiKey, widgetSecret: onramperWidgetSecret } = getOnramperConfig(env);
|
|
162
|
-
|
|
163
|
-
return [
|
|
164
|
-
{
|
|
165
|
-
id: "manual",
|
|
166
|
-
label: "Receive on Base",
|
|
167
|
-
description: "Send ETH or USDC on Base directly to this wallet address.",
|
|
168
|
-
available: true,
|
|
169
|
-
kind: "direct",
|
|
170
|
-
address,
|
|
171
|
-
explorer_url: `${BASE_EXPLORER_URL}/address/${address}`,
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
id: "transak",
|
|
175
|
-
label: "Transak",
|
|
176
|
-
description: "Direct card or bank checkout for Base USDC.",
|
|
177
|
-
available: Boolean(transakApiKey && transakSecret),
|
|
178
|
-
kind: "direct",
|
|
179
|
-
action_hint: "Call apiosk_configure with section='funding' and funding_provider='transak' to request a checkout URL.",
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
id: "onramper",
|
|
183
|
-
label: "Onramper",
|
|
184
|
-
description: "Multi-provider onramp with outsourced KYC.",
|
|
185
|
-
available: Boolean(onramperApiKey && onramperWidgetSecret),
|
|
186
|
-
kind: "aggregator",
|
|
187
|
-
action_hint: "Call apiosk_configure with section='funding' and funding_provider='onramper' to request a checkout URL.",
|
|
188
|
-
},
|
|
189
|
-
];
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export async function buildFundingOptions({
|
|
193
|
-
wallet,
|
|
194
|
-
env = process.env,
|
|
195
|
-
fetchImpl = globalThis.fetch,
|
|
196
|
-
fundingProvider = null,
|
|
197
|
-
includeQrDataUrl = false,
|
|
198
|
-
} = {}) {
|
|
199
|
-
if (!wallet?.address) {
|
|
200
|
-
return null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const address = trimString(wallet.address).toLowerCase();
|
|
204
|
-
const receiveOnBase = {
|
|
205
|
-
title: "Receive on Base",
|
|
206
|
-
description: "This address accepts both ETH and USDC on Base.",
|
|
207
|
-
address,
|
|
208
|
-
network: "Base",
|
|
209
|
-
accepted_assets: ["ETH", "USDC"],
|
|
210
|
-
explorer_url: `${BASE_EXPLORER_URL}/address/${address}`,
|
|
211
|
-
...await buildQrBundle(address, { env, includeQrDataUrl }),
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const providers = buildFundingProviders(address, env);
|
|
215
|
-
let selectedProvider = null;
|
|
216
|
-
const requestedProvider = trimString(fundingProvider).toLowerCase();
|
|
217
|
-
|
|
218
|
-
if (requestedProvider && requestedProvider !== "manual") {
|
|
219
|
-
const provider = providers.find((entry) => entry.id === requestedProvider);
|
|
220
|
-
if (provider) {
|
|
221
|
-
if (!provider.available) {
|
|
222
|
-
selectedProvider = {
|
|
223
|
-
...provider,
|
|
224
|
-
error: `${provider.label} is not configured in this environment.`,
|
|
225
|
-
};
|
|
226
|
-
} else if (provider.id === "onramper") {
|
|
227
|
-
selectedProvider = {
|
|
228
|
-
...provider,
|
|
229
|
-
widget_url: buildOnramperWidgetUrl(address, env),
|
|
230
|
-
opens_in: "browser",
|
|
231
|
-
};
|
|
232
|
-
} else if (provider.id === "transak") {
|
|
233
|
-
const session = await createTransakFundingSession(address, env, fetchImpl);
|
|
234
|
-
selectedProvider = {
|
|
235
|
-
...provider,
|
|
236
|
-
...session,
|
|
237
|
-
opens_in: "browser",
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
} else if (requestedProvider === "manual") {
|
|
242
|
-
selectedProvider = providers.find((entry) => entry.id === "manual") || null;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return {
|
|
246
|
-
receive_on_base: receiveOnBase,
|
|
247
|
-
providers,
|
|
248
|
-
selected_provider: selectedProvider,
|
|
249
|
-
notes: [
|
|
250
|
-
"Base mainnet only.",
|
|
251
|
-
"Manual funding works even when embedded onramp providers are unavailable.",
|
|
252
|
-
"Use the wallet address or QR code from this section to top up the wallet.",
|
|
253
|
-
],
|
|
254
|
-
};
|
|
255
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
4
|
-
|
|
5
|
-
function parseBody(text) {
|
|
6
|
-
if (!text) return null;
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
return JSON.parse(text);
|
|
10
|
-
} catch {
|
|
11
|
-
return text;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function pickMessage(payload, fallback) {
|
|
16
|
-
if (payload && typeof payload === "object") {
|
|
17
|
-
if (typeof payload.message === "string" && payload.message.trim()) {
|
|
18
|
-
return payload.message.trim();
|
|
19
|
-
}
|
|
20
|
-
if (typeof payload.error === "string" && payload.error.trim()) {
|
|
21
|
-
return payload.error.trim();
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (typeof payload === "string" && payload.trim()) {
|
|
26
|
-
return payload.trim();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return fallback;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function buildWalletAuthMessage(action, resource, walletAddress, timestamp, nonce) {
|
|
33
|
-
return [
|
|
34
|
-
"Apiosk auth",
|
|
35
|
-
`action:${action}`,
|
|
36
|
-
`wallet:${String(walletAddress || "").toLowerCase()}`,
|
|
37
|
-
`resource:${resource}`,
|
|
38
|
-
`timestamp:${timestamp}`,
|
|
39
|
-
`nonce:${nonce}`,
|
|
40
|
-
].join("\n");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function signedRequest({
|
|
44
|
-
baseUrl,
|
|
45
|
-
path,
|
|
46
|
-
method = "GET",
|
|
47
|
-
body,
|
|
48
|
-
action,
|
|
49
|
-
resource,
|
|
50
|
-
wallet,
|
|
51
|
-
fetchImpl = globalThis.fetch,
|
|
52
|
-
}) {
|
|
53
|
-
if (!fetchImpl) {
|
|
54
|
-
throw new Error("A fetch implementation is required.");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const account = privateKeyToAccount(wallet.private_key);
|
|
58
|
-
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
59
|
-
const nonce = randomBytes(16).toString("hex");
|
|
60
|
-
const message = buildWalletAuthMessage(action, resource, wallet.address, timestamp, nonce);
|
|
61
|
-
const signature = await account.signMessage({ message });
|
|
62
|
-
const headers = new Headers({
|
|
63
|
-
accept: "application/json",
|
|
64
|
-
"x-wallet-address": wallet.address,
|
|
65
|
-
"x-wallet-signature": signature,
|
|
66
|
-
"x-wallet-timestamp": timestamp,
|
|
67
|
-
"x-wallet-nonce": nonce,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const init = {
|
|
71
|
-
method,
|
|
72
|
-
headers,
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
if (body !== undefined) {
|
|
76
|
-
headers.set("content-type", "application/json");
|
|
77
|
-
init.body = JSON.stringify(body);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const response = await fetchImpl(`${String(baseUrl).replace(/\/+$/, "")}${path}`, init);
|
|
81
|
-
const text = await response.text();
|
|
82
|
-
const payload = parseBody(text);
|
|
83
|
-
|
|
84
|
-
return { payload, response };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function requestGatewayManagement(options) {
|
|
88
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
89
|
-
const { response, payload } = await signedRequest(options);
|
|
90
|
-
if (response.ok) {
|
|
91
|
-
return payload;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if ((response.status === 401 || response.status === 403) && attempt === 0) {
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
throw new Error(
|
|
99
|
-
pickMessage(
|
|
100
|
-
payload,
|
|
101
|
-
`Gateway management request failed with HTTP ${response.status}`
|
|
102
|
-
)
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
throw new Error("Gateway management request failed.");
|
|
107
|
-
}
|
package/src/listing-metadata.mjs
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
function isObject(value) {
|
|
2
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
function cloneJson(value, fallback) {
|
|
6
|
-
if (value === undefined) return fallback;
|
|
7
|
-
return JSON.parse(JSON.stringify(value));
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function normalizePath(pathValue) {
|
|
11
|
-
const value = String(pathValue || "/").trim();
|
|
12
|
-
if (!value || value === "/") return "/";
|
|
13
|
-
return value.startsWith("/") ? value : `/${value}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function normalizeTags(tags = []) {
|
|
17
|
-
return Array.from(
|
|
18
|
-
new Set(
|
|
19
|
-
tags
|
|
20
|
-
.map((tag) => String(tag || "").trim().toLowerCase())
|
|
21
|
-
.filter(Boolean)
|
|
22
|
-
)
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function defaultExecuteInputSchema() {
|
|
27
|
-
return {
|
|
28
|
-
type: "object",
|
|
29
|
-
additionalProperties: false,
|
|
30
|
-
properties: {
|
|
31
|
-
operation: {
|
|
32
|
-
type: "string",
|
|
33
|
-
description: "Optional operation selector. Accepts an operation id, a path, or a METHOD /path key.",
|
|
34
|
-
},
|
|
35
|
-
input: {
|
|
36
|
-
description: "JSON payload forwarded to the selected operation.",
|
|
37
|
-
},
|
|
38
|
-
query: {
|
|
39
|
-
type: "object",
|
|
40
|
-
additionalProperties: true,
|
|
41
|
-
description: "Optional query parameter override for GET-style operations.",
|
|
42
|
-
},
|
|
43
|
-
path_params: {
|
|
44
|
-
type: "object",
|
|
45
|
-
additionalProperties: true,
|
|
46
|
-
description: "Optional path parameter override for routes that include placeholders.",
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function defaultExecuteOutputSchema() {
|
|
53
|
-
return {
|
|
54
|
-
type: "object",
|
|
55
|
-
required: ["status", "result", "cost", "latency", "operation", "api", "upstream_status"],
|
|
56
|
-
properties: {
|
|
57
|
-
status: { type: "string", enum: ["success", "error"] },
|
|
58
|
-
result: {},
|
|
59
|
-
cost: { type: "number" },
|
|
60
|
-
latency: { type: "integer" },
|
|
61
|
-
operation: { type: "string" },
|
|
62
|
-
api: { type: "string" },
|
|
63
|
-
upstream_status: { type: "integer" },
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function defaultOperations(priceUsd) {
|
|
69
|
-
return [
|
|
70
|
-
{
|
|
71
|
-
id: "/",
|
|
72
|
-
key: "GET /",
|
|
73
|
-
method: "GET",
|
|
74
|
-
path: "/",
|
|
75
|
-
price_usd: priceUsd,
|
|
76
|
-
payment_required: true,
|
|
77
|
-
description: "Forward a GET request to the API root.",
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
id: "/",
|
|
81
|
-
key: "POST /",
|
|
82
|
-
method: "POST",
|
|
83
|
-
path: "/",
|
|
84
|
-
price_usd: priceUsd,
|
|
85
|
-
payment_required: true,
|
|
86
|
-
description: "Forward a POST request to the API root.",
|
|
87
|
-
},
|
|
88
|
-
];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function normalizeOperation(operation, priceUsd) {
|
|
92
|
-
const method = String(operation?.method || "POST").trim().toUpperCase();
|
|
93
|
-
const path = normalizePath(operation?.path || operation?.id || "/");
|
|
94
|
-
return {
|
|
95
|
-
...cloneJson(operation, {}),
|
|
96
|
-
id: String(operation?.id || path),
|
|
97
|
-
key: String(operation?.key || `${method} ${path}`),
|
|
98
|
-
method,
|
|
99
|
-
path,
|
|
100
|
-
price_usd:
|
|
101
|
-
typeof operation?.price_usd === "number" && Number.isFinite(operation.price_usd)
|
|
102
|
-
? operation.price_usd
|
|
103
|
-
: priceUsd,
|
|
104
|
-
payment_required:
|
|
105
|
-
typeof operation?.payment_required === "boolean" ? operation.payment_required : true,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function isDiagnosticOperationPath(path = "/") {
|
|
110
|
-
const normalized = normalizePath(path);
|
|
111
|
-
if (normalized === "/") return true;
|
|
112
|
-
return normalized.split("/").filter(Boolean).some((segment) =>
|
|
113
|
-
/^(health|status|metadata|docs?|openapi|swagger|ping|readyz?|livez?)$/i.test(segment)
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function defaultOperationRank(operation = {}) {
|
|
118
|
-
const path = normalizePath(operation.path || operation.id || "/");
|
|
119
|
-
const trimmed = path.replace(/^\/+|\/+$/g, "");
|
|
120
|
-
const segments = trimmed ? trimmed.split("/").filter(Boolean).length : 0;
|
|
121
|
-
const method = String(operation.method || "POST").trim().toUpperCase();
|
|
122
|
-
const methodRank =
|
|
123
|
-
method === "POST"
|
|
124
|
-
? 0
|
|
125
|
-
: method === "PUT"
|
|
126
|
-
? 1
|
|
127
|
-
: method === "PATCH"
|
|
128
|
-
? 2
|
|
129
|
-
: method === "DELETE"
|
|
130
|
-
? 3
|
|
131
|
-
: method === "GET"
|
|
132
|
-
? 4
|
|
133
|
-
: method === "HEAD"
|
|
134
|
-
? 5
|
|
135
|
-
: 6;
|
|
136
|
-
return [
|
|
137
|
-
isDiagnosticOperationPath(path) ? 1 : 0,
|
|
138
|
-
/batch/i.test(path) ? 1 : 0,
|
|
139
|
-
segments,
|
|
140
|
-
path.length,
|
|
141
|
-
methodRank,
|
|
142
|
-
operation.payment_required === false ? 1 : 0,
|
|
143
|
-
`${method} ${path}`,
|
|
144
|
-
];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function inferDefaultOperation(operations = []) {
|
|
148
|
-
const ranked = [...operations].sort((left, right) => {
|
|
149
|
-
const leftRank = defaultOperationRank(left);
|
|
150
|
-
const rightRank = defaultOperationRank(right);
|
|
151
|
-
for (let index = 0; index < leftRank.length; index += 1) {
|
|
152
|
-
if (leftRank[index] < rightRank[index]) return -1;
|
|
153
|
-
if (leftRank[index] > rightRank[index]) return 1;
|
|
154
|
-
}
|
|
155
|
-
return 0;
|
|
156
|
-
});
|
|
157
|
-
return ranked[0]?.id || operations[0]?.id || "/";
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function inferReadOnlyHint(operations = [], slug = "", name = "", description = "") {
|
|
161
|
-
const allReadOnly =
|
|
162
|
-
operations.length > 0 &&
|
|
163
|
-
operations.every((operation) => operation.method === "GET" || operation.method === "HEAD");
|
|
164
|
-
|
|
165
|
-
if (allReadOnly) return true;
|
|
166
|
-
|
|
167
|
-
return /extract|lookup|watch|map|research|digest|diff|monitor|verify|metadata|status/i.test(
|
|
168
|
-
`${slug} ${name} ${description}`
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function inferIdempotentHint(operations = []) {
|
|
173
|
-
return (
|
|
174
|
-
operations.length > 0 &&
|
|
175
|
-
operations.every((operation) => operation.method === "GET" || operation.method === "HEAD")
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function normalizeListingGroup(listingGroup) {
|
|
180
|
-
const value = String(listingGroup || "").trim().toLowerCase();
|
|
181
|
-
return value || null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function resolveCategory(category, listingGroup) {
|
|
185
|
-
const explicit = String(category || "").trim().toLowerCase();
|
|
186
|
-
if (explicit) return explicit;
|
|
187
|
-
|
|
188
|
-
const normalizedGroup = normalizeListingGroup(listingGroup);
|
|
189
|
-
if (normalizedGroup === "datasets") return "dataset";
|
|
190
|
-
if (normalizedGroup === "compute") return "compute";
|
|
191
|
-
return "data";
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function resolveProviderListingType(category, listingGroup, baseProvider = null) {
|
|
195
|
-
const explicit = String(baseProvider?.listing_type || "").trim().toLowerCase();
|
|
196
|
-
if (explicit) return explicit;
|
|
197
|
-
|
|
198
|
-
const normalizedGroup = normalizeListingGroup(listingGroup);
|
|
199
|
-
if (normalizedGroup === "datasets" || category === "dataset") return "dataset";
|
|
200
|
-
if (["service", "connector", "skill", "product"].includes(normalizedGroup || "")) {
|
|
201
|
-
return normalizedGroup;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return "api";
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export function buildListingMetadata({
|
|
208
|
-
name,
|
|
209
|
-
slug,
|
|
210
|
-
description,
|
|
211
|
-
endpoint_url,
|
|
212
|
-
price_usd,
|
|
213
|
-
category,
|
|
214
|
-
listing_group,
|
|
215
|
-
listing_metadata = {},
|
|
216
|
-
} = {}) {
|
|
217
|
-
const safeSlug = String(slug || "").trim();
|
|
218
|
-
const safeName = String(name || safeSlug || "Apiosk API").trim();
|
|
219
|
-
const safeDescription = String(description || "").trim();
|
|
220
|
-
const safeEndpointUrl = String(endpoint_url || "").trim();
|
|
221
|
-
const safePrice = Number(price_usd);
|
|
222
|
-
const normalizedCategory = resolveCategory(category, listing_group);
|
|
223
|
-
const base = isObject(listing_metadata) ? cloneJson(listing_metadata, {}) : {};
|
|
224
|
-
const operationsInput = Array.isArray(base.operations) ? base.operations : defaultOperations(safePrice);
|
|
225
|
-
const operations = operationsInput.map((operation) => normalizeOperation(operation, safePrice));
|
|
226
|
-
const inputSchema = cloneJson(
|
|
227
|
-
base.input_schema || base.mcp_tool?.inputSchema,
|
|
228
|
-
defaultExecuteInputSchema()
|
|
229
|
-
);
|
|
230
|
-
const outputSchema = cloneJson(
|
|
231
|
-
base.output_schema || base.mcp_tool?.outputSchema,
|
|
232
|
-
defaultExecuteOutputSchema()
|
|
233
|
-
);
|
|
234
|
-
const provider = {
|
|
235
|
-
type: "http_api",
|
|
236
|
-
origin_url: safeEndpointUrl,
|
|
237
|
-
...cloneJson(base.provider, {}),
|
|
238
|
-
origin_url: safeEndpointUrl,
|
|
239
|
-
listing_type: resolveProviderListingType(normalizedCategory, listing_group, base.provider),
|
|
240
|
-
};
|
|
241
|
-
const mergedTags = normalizeTags([
|
|
242
|
-
...(Array.isArray(base.tags) ? base.tags : []),
|
|
243
|
-
normalizedCategory,
|
|
244
|
-
"api",
|
|
245
|
-
"agent-ready",
|
|
246
|
-
"mcp-native",
|
|
247
|
-
]);
|
|
248
|
-
const defaultOperation = String(base.default_operation || inferDefaultOperation(operations));
|
|
249
|
-
const readOnlyHint =
|
|
250
|
-
typeof base.mcp_tool?.annotations?.readOnlyHint === "boolean"
|
|
251
|
-
? base.mcp_tool.annotations.readOnlyHint
|
|
252
|
-
: inferReadOnlyHint(operations, safeSlug, safeName, safeDescription);
|
|
253
|
-
const idempotentHint =
|
|
254
|
-
typeof base.mcp_tool?.annotations?.idempotentHint === "boolean"
|
|
255
|
-
? base.mcp_tool.annotations.idempotentHint
|
|
256
|
-
: inferIdempotentHint(operations);
|
|
257
|
-
|
|
258
|
-
return {
|
|
259
|
-
...base,
|
|
260
|
-
agent_native: true,
|
|
261
|
-
mcp_native: true,
|
|
262
|
-
metadata_path: String(base.metadata_path || "/metadata"),
|
|
263
|
-
execute_path: String(base.execute_path || "/execute"),
|
|
264
|
-
default_operation: defaultOperation,
|
|
265
|
-
cost_per_call: safePrice,
|
|
266
|
-
input_schema: inputSchema,
|
|
267
|
-
output_schema: outputSchema,
|
|
268
|
-
operations,
|
|
269
|
-
provider,
|
|
270
|
-
tags: mergedTags,
|
|
271
|
-
mcp_tool: {
|
|
272
|
-
...cloneJson(base.mcp_tool, {}),
|
|
273
|
-
name: String(base.mcp_tool?.name || safeSlug),
|
|
274
|
-
title: String(base.mcp_tool?.title || safeName),
|
|
275
|
-
description: String(base.mcp_tool?.description || safeDescription || safeName),
|
|
276
|
-
inputSchema: inputSchema,
|
|
277
|
-
outputSchema: outputSchema,
|
|
278
|
-
annotations: {
|
|
279
|
-
readOnlyHint,
|
|
280
|
-
idempotentHint,
|
|
281
|
-
destructiveHint: false,
|
|
282
|
-
openWorldHint: false,
|
|
283
|
-
...cloneJson(base.mcp_tool?.annotations, {}),
|
|
284
|
-
},
|
|
285
|
-
},
|
|
286
|
-
};
|
|
287
|
-
}
|