opentool 0.10.5 → 0.12.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/dist/adapters/hyperliquid/index.d.ts +1 -1
- package/dist/adapters/polymarket/index.d.ts +319 -0
- package/dist/adapters/polymarket/index.js +831 -0
- package/dist/adapters/polymarket/index.js.map +1 -0
- package/dist/backtest/index.d.ts +58 -0
- package/dist/backtest/index.js +122 -0
- package/dist/backtest/index.js.map +1 -0
- package/dist/cli/index.d.ts +17 -4
- package/dist/{validate-DbhJ_r0Z.d.ts → index-9Z3wo28l.d.ts} +2 -25
- package/dist/index-BnQsRvND.d.ts +13 -0
- package/dist/index.d.ts +8 -376
- package/dist/index.js +29 -1110
- package/dist/index.js.map +1 -1
- package/dist/{payment-orkZA9se.d.ts → payment-BLm1ltur.d.ts} +1 -1
- package/dist/{types-rAQrDrah.d.ts → types-BaTmu0gS.d.ts} +1 -1
- package/dist/validation/index.d.ts +4 -0
- package/dist/validation/index.js +1255 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/wallet/browser.d.ts +1 -1
- package/dist/wallet/index.d.ts +2 -2
- package/dist/x402/index.d.ts +1 -1
- package/package.json +9 -1
- package/templates/base/package.json +1 -1
|
@@ -0,0 +1,1255 @@
|
|
|
1
|
+
import * as fs3 from 'fs';
|
|
2
|
+
import * as path4 from 'path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { zodToJsonSchema } from '@alcyone-labs/zod-to-json-schema';
|
|
6
|
+
import { tmpdir } from 'os';
|
|
7
|
+
import { build } from 'esbuild';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
|
|
10
|
+
// src/cli/validate.ts
|
|
11
|
+
var X402_VERSION = 1;
|
|
12
|
+
var HEADER_X402 = "X-PAYMENT";
|
|
13
|
+
var HEADER_PAYMENT_RESPONSE = "X-PAYMENT-RESPONSE";
|
|
14
|
+
var x402RequirementSchema = z.object({
|
|
15
|
+
scheme: z.string().min(1),
|
|
16
|
+
network: z.string().min(1),
|
|
17
|
+
maxAmountRequired: z.string().min(1),
|
|
18
|
+
asset: z.string().min(1),
|
|
19
|
+
payTo: z.string().min(1),
|
|
20
|
+
resource: z.string().optional(),
|
|
21
|
+
description: z.string().optional(),
|
|
22
|
+
mimeType: z.string().optional(),
|
|
23
|
+
outputSchema: z.unknown().optional(),
|
|
24
|
+
maxTimeoutSeconds: z.number().int().positive().optional(),
|
|
25
|
+
extra: z.record(z.string(), z.unknown()).nullable().optional()
|
|
26
|
+
});
|
|
27
|
+
var x402PaymentHeaderSchema = z.object({
|
|
28
|
+
x402Version: z.number().int().positive(),
|
|
29
|
+
scheme: z.string().min(1),
|
|
30
|
+
network: z.string().min(1),
|
|
31
|
+
correlationId: z.string().optional(),
|
|
32
|
+
payload: z.unknown()
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// src/x402/helpers.ts
|
|
36
|
+
function createX402PaymentRequired(definition) {
|
|
37
|
+
const requirement = toX402Requirement(definition);
|
|
38
|
+
const body = {
|
|
39
|
+
schemaVersion: 1,
|
|
40
|
+
message: definition.description ?? "Payment required",
|
|
41
|
+
resource: definition.resource,
|
|
42
|
+
accepts: [
|
|
43
|
+
{
|
|
44
|
+
id: "x402",
|
|
45
|
+
title: `Pay ${definition.amount} ${definition.currency.code}`,
|
|
46
|
+
description: definition.description,
|
|
47
|
+
amount: {
|
|
48
|
+
value: definition.amount,
|
|
49
|
+
currency: {
|
|
50
|
+
code: definition.currency.code,
|
|
51
|
+
symbol: definition.currency.symbol,
|
|
52
|
+
decimals: definition.currency.decimals,
|
|
53
|
+
kind: "crypto"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
asset: {
|
|
57
|
+
symbol: definition.asset.symbol,
|
|
58
|
+
network: definition.asset.network,
|
|
59
|
+
address: definition.asset.address,
|
|
60
|
+
decimals: definition.asset.decimals,
|
|
61
|
+
standard: "erc20"
|
|
62
|
+
},
|
|
63
|
+
payTo: definition.payTo,
|
|
64
|
+
resource: definition.resource,
|
|
65
|
+
proof: {
|
|
66
|
+
mode: "x402",
|
|
67
|
+
scheme: definition.scheme,
|
|
68
|
+
network: definition.network,
|
|
69
|
+
version: X402_VERSION,
|
|
70
|
+
facilitator: definition.facilitator,
|
|
71
|
+
verifier: "x402:facilitator"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
metadata: definition.metadata ?? {},
|
|
76
|
+
x402: {
|
|
77
|
+
x402Version: X402_VERSION,
|
|
78
|
+
error: definition.description ?? "Payment required",
|
|
79
|
+
accepts: [requirement]
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return new Response(JSON.stringify(body), {
|
|
83
|
+
status: 402,
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "application/json"
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function extractX402Attempt(request) {
|
|
90
|
+
const raw = request.headers.get(HEADER_X402);
|
|
91
|
+
if (!raw) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const payload = decodeJson(raw, x402PaymentHeaderSchema);
|
|
96
|
+
return {
|
|
97
|
+
type: "x402",
|
|
98
|
+
headerName: HEADER_X402,
|
|
99
|
+
raw,
|
|
100
|
+
payload
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function verifyX402Payment(attempt, definition, options = {}) {
|
|
107
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
108
|
+
const timeout = options.timeout ?? 25e3;
|
|
109
|
+
const facilitator = definition.facilitator;
|
|
110
|
+
const verifierUrl = new URL(
|
|
111
|
+
facilitator.verifyPath ?? "/verify",
|
|
112
|
+
ensureTrailingSlash(facilitator.url)
|
|
113
|
+
).toString();
|
|
114
|
+
const requirement = toX402Requirement(definition);
|
|
115
|
+
const headers = buildFacilitatorHeaders(facilitator);
|
|
116
|
+
try {
|
|
117
|
+
const verifyBody = {
|
|
118
|
+
x402Version: attempt.payload.x402Version,
|
|
119
|
+
paymentPayload: attempt.payload,
|
|
120
|
+
paymentRequirements: requirement
|
|
121
|
+
};
|
|
122
|
+
console.log("[x402] Calling facilitator /verify", {
|
|
123
|
+
url: verifierUrl,
|
|
124
|
+
fullBody: JSON.stringify(verifyBody, null, 2)
|
|
125
|
+
});
|
|
126
|
+
const verifyResponse = await Promise.race([
|
|
127
|
+
fetchImpl(verifierUrl, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers,
|
|
130
|
+
body: JSON.stringify(verifyBody)
|
|
131
|
+
}),
|
|
132
|
+
new Promise(
|
|
133
|
+
(_, reject) => setTimeout(() => reject(new Error(`Verification timeout after ${timeout}ms`)), timeout)
|
|
134
|
+
)
|
|
135
|
+
]);
|
|
136
|
+
console.log("[x402] Facilitator /verify response", { status: verifyResponse.status });
|
|
137
|
+
if (!verifyResponse.ok) {
|
|
138
|
+
const errorText = await verifyResponse.text().catch(() => "");
|
|
139
|
+
console.error("[x402] Facilitator /verify error", {
|
|
140
|
+
status: verifyResponse.status,
|
|
141
|
+
body: errorText
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
success: false,
|
|
145
|
+
failure: {
|
|
146
|
+
reason: `Facilitator verify request failed: ${verifyResponse.status}${errorText ? ` - ${errorText}` : ""}`,
|
|
147
|
+
code: "verification_failed"
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const verifyPayload = await verifyResponse.json();
|
|
152
|
+
if (!verifyPayload.isValid) {
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
failure: {
|
|
156
|
+
reason: verifyPayload.invalidReason ?? "Facilitator verification failed",
|
|
157
|
+
code: "verification_failed"
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const responseHeaders = {};
|
|
162
|
+
if (options.settle) {
|
|
163
|
+
const settleUrl = new URL(
|
|
164
|
+
facilitator.settlePath ?? "/settle",
|
|
165
|
+
ensureTrailingSlash(facilitator.url)
|
|
166
|
+
).toString();
|
|
167
|
+
try {
|
|
168
|
+
const settleBody = {
|
|
169
|
+
x402Version: attempt.payload.x402Version,
|
|
170
|
+
paymentPayload: attempt.payload,
|
|
171
|
+
paymentRequirements: requirement
|
|
172
|
+
};
|
|
173
|
+
console.log("[x402] Calling facilitator /settle", {
|
|
174
|
+
url: settleUrl,
|
|
175
|
+
bodyPreview: JSON.stringify(settleBody).substring(0, 300)
|
|
176
|
+
});
|
|
177
|
+
const settleResponse = await Promise.race([
|
|
178
|
+
fetchImpl(settleUrl, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers,
|
|
181
|
+
body: JSON.stringify(settleBody)
|
|
182
|
+
}),
|
|
183
|
+
new Promise(
|
|
184
|
+
(_, reject) => setTimeout(() => reject(new Error(`Settlement timeout after ${timeout}ms`)), timeout)
|
|
185
|
+
)
|
|
186
|
+
]);
|
|
187
|
+
console.log("[x402] Facilitator /settle response", { status: settleResponse.status });
|
|
188
|
+
if (!settleResponse.ok) {
|
|
189
|
+
const errorText = await settleResponse.text().catch(() => "");
|
|
190
|
+
console.error("[x402] Facilitator /settle error", {
|
|
191
|
+
status: settleResponse.status,
|
|
192
|
+
body: errorText
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
failure: {
|
|
197
|
+
reason: `Facilitator settlement failed: ${settleResponse.status}${errorText ? ` - ${errorText}` : ""}`,
|
|
198
|
+
code: "settlement_failed"
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const settlePayload = await settleResponse.json();
|
|
203
|
+
console.log("[x402] Facilitator /settle success", { txHash: settlePayload.txHash });
|
|
204
|
+
if (settlePayload.txHash) {
|
|
205
|
+
responseHeaders[HEADER_PAYMENT_RESPONSE] = JSON.stringify({
|
|
206
|
+
settled: true,
|
|
207
|
+
txHash: settlePayload.txHash
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error("[x402] Settlement exception", {
|
|
212
|
+
error: error instanceof Error ? error.message : String(error)
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
failure: {
|
|
217
|
+
reason: error instanceof Error ? error.message : "Settlement failed",
|
|
218
|
+
code: "settlement_failed"
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const result = {
|
|
224
|
+
success: true,
|
|
225
|
+
metadata: {
|
|
226
|
+
optionId: "x402",
|
|
227
|
+
verifier: "x402:facilitator",
|
|
228
|
+
amount: definition.amount,
|
|
229
|
+
currency: definition.currency.code,
|
|
230
|
+
network: definition.network
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
if (Object.keys(responseHeaders).length > 0) {
|
|
234
|
+
result.responseHeaders = responseHeaders;
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return {
|
|
239
|
+
success: false,
|
|
240
|
+
failure: {
|
|
241
|
+
reason: error instanceof Error ? error.message : "Unknown error",
|
|
242
|
+
code: "verification_failed"
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function toX402Requirement(definition) {
|
|
248
|
+
const decimals = definition.asset.decimals;
|
|
249
|
+
const units = decimalToBaseUnits(definition.amount, decimals);
|
|
250
|
+
return x402RequirementSchema.parse({
|
|
251
|
+
scheme: definition.scheme,
|
|
252
|
+
network: definition.network,
|
|
253
|
+
maxAmountRequired: units,
|
|
254
|
+
asset: definition.asset.address,
|
|
255
|
+
payTo: definition.payTo,
|
|
256
|
+
resource: definition.resource,
|
|
257
|
+
description: definition.description,
|
|
258
|
+
mimeType: "application/json",
|
|
259
|
+
maxTimeoutSeconds: 900,
|
|
260
|
+
extra: {
|
|
261
|
+
symbol: definition.asset.symbol,
|
|
262
|
+
currencyCode: definition.currency.code,
|
|
263
|
+
decimals
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function decimalToBaseUnits(value, decimals) {
|
|
268
|
+
const [whole, fraction = ""] = value.split(".");
|
|
269
|
+
const sanitizedFraction = fraction.slice(0, decimals);
|
|
270
|
+
const paddedFraction = sanitizedFraction.padEnd(decimals, "0");
|
|
271
|
+
const combined = `${whole}${paddedFraction}`.replace(/^0+/, "");
|
|
272
|
+
return combined.length > 0 ? combined : "0";
|
|
273
|
+
}
|
|
274
|
+
function decodeJson(value, schema) {
|
|
275
|
+
const base64 = normalizeBase64(value);
|
|
276
|
+
const json = Buffer.from(base64, "base64").toString("utf-8");
|
|
277
|
+
const parsed = JSON.parse(json);
|
|
278
|
+
return schema.parse(parsed);
|
|
279
|
+
}
|
|
280
|
+
function normalizeBase64(input) {
|
|
281
|
+
if (/^[A-Za-z0-9+/=]+$/.test(input)) {
|
|
282
|
+
return input;
|
|
283
|
+
}
|
|
284
|
+
const restored = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
285
|
+
const paddingNeeded = (4 - restored.length % 4) % 4;
|
|
286
|
+
return restored + "=".repeat(paddingNeeded);
|
|
287
|
+
}
|
|
288
|
+
function buildFacilitatorHeaders(facilitator) {
|
|
289
|
+
const headers = {
|
|
290
|
+
"Content-Type": "application/json"
|
|
291
|
+
};
|
|
292
|
+
if (facilitator.apiKeyHeader && process.env.X402_FACILITATOR_API_KEY) {
|
|
293
|
+
headers[facilitator.apiKeyHeader] = process.env.X402_FACILITATOR_API_KEY;
|
|
294
|
+
}
|
|
295
|
+
return headers;
|
|
296
|
+
}
|
|
297
|
+
function ensureTrailingSlash(url) {
|
|
298
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/x402/payment.ts
|
|
302
|
+
var PAYMENT_CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("opentool.x402.context");
|
|
303
|
+
var X402PaymentRequiredError = class extends Error {
|
|
304
|
+
constructor(response, verification) {
|
|
305
|
+
super("X402 Payment required");
|
|
306
|
+
this.name = "X402PaymentRequiredError";
|
|
307
|
+
this.response = response;
|
|
308
|
+
this.verification = verification;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
function setPaymentContext(request, context) {
|
|
312
|
+
try {
|
|
313
|
+
Object.defineProperty(request, PAYMENT_CONTEXT_SYMBOL, {
|
|
314
|
+
value: context,
|
|
315
|
+
configurable: true,
|
|
316
|
+
enumerable: false,
|
|
317
|
+
writable: true
|
|
318
|
+
});
|
|
319
|
+
} catch {
|
|
320
|
+
request[PAYMENT_CONTEXT_SYMBOL] = context;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function requireX402Payment(request, payment, options = {}) {
|
|
324
|
+
const definition = isX402Payment(payment) ? payment.definition : payment;
|
|
325
|
+
const attempt = extractX402Attempt(request);
|
|
326
|
+
if (!attempt) {
|
|
327
|
+
const response = createX402PaymentRequired(definition);
|
|
328
|
+
throw new X402PaymentRequiredError(response);
|
|
329
|
+
}
|
|
330
|
+
const verifyOptions = {
|
|
331
|
+
settle: options.settle !== void 0 ? options.settle : true
|
|
332
|
+
};
|
|
333
|
+
if (options.fetchImpl !== void 0) {
|
|
334
|
+
verifyOptions.fetchImpl = options.fetchImpl;
|
|
335
|
+
}
|
|
336
|
+
const verification = await verifyX402Payment(attempt, definition, verifyOptions);
|
|
337
|
+
if (!verification.success || !verification.metadata) {
|
|
338
|
+
if (options.onFailure) {
|
|
339
|
+
return options.onFailure(verification);
|
|
340
|
+
}
|
|
341
|
+
const response = createX402PaymentRequired(definition);
|
|
342
|
+
throw new X402PaymentRequiredError(response, verification);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
payment: verification.metadata,
|
|
346
|
+
headers: verification.responseHeaders ?? {},
|
|
347
|
+
result: verification
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function withX402Payment(handler, payment, options = {}) {
|
|
351
|
+
return async (request) => {
|
|
352
|
+
const verification = await requireX402Payment(request, payment, options);
|
|
353
|
+
if (verification instanceof Response) {
|
|
354
|
+
return verification;
|
|
355
|
+
}
|
|
356
|
+
setPaymentContext(request, verification);
|
|
357
|
+
const response = await Promise.resolve(handler(request));
|
|
358
|
+
return applyPaymentHeaders(response, verification.headers);
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function applyPaymentHeaders(response, headers) {
|
|
362
|
+
const entries = Object.entries(headers ?? {});
|
|
363
|
+
if (entries.length === 0) {
|
|
364
|
+
return response;
|
|
365
|
+
}
|
|
366
|
+
let mutated = false;
|
|
367
|
+
const merged = new Headers(response.headers);
|
|
368
|
+
for (const [key, value] of entries) {
|
|
369
|
+
if (!merged.has(key)) {
|
|
370
|
+
merged.set(key, value);
|
|
371
|
+
mutated = true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (!mutated) {
|
|
375
|
+
return response;
|
|
376
|
+
}
|
|
377
|
+
return new Response(response.body, {
|
|
378
|
+
status: response.status,
|
|
379
|
+
statusText: response.statusText,
|
|
380
|
+
headers: merged
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
function isX402Payment(value) {
|
|
384
|
+
return !!value && typeof value === "object" && "definition" in value && value.definition !== void 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/adapters/mcp.ts
|
|
388
|
+
function createMcpAdapter(options) {
|
|
389
|
+
const normalizedSchema = ensureSchema(options.schema);
|
|
390
|
+
const defaultMethod = resolveDefaultMethod(options);
|
|
391
|
+
const httpHandler = options.httpHandlers[defaultMethod];
|
|
392
|
+
if (!httpHandler) {
|
|
393
|
+
throw new Error(`Tool "${options.name}" does not export an HTTP handler for ${defaultMethod}`);
|
|
394
|
+
}
|
|
395
|
+
return async function invoke(rawArguments) {
|
|
396
|
+
const validated = normalizedSchema ? normalizedSchema.parse(rawArguments ?? {}) : rawArguments;
|
|
397
|
+
const request = buildRequest(options.name, defaultMethod, validated);
|
|
398
|
+
try {
|
|
399
|
+
const response = await Promise.resolve(httpHandler(request));
|
|
400
|
+
return await responseToToolResponse(response);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (error instanceof X402PaymentRequiredError) {
|
|
403
|
+
return await responseToToolResponse(error.response);
|
|
404
|
+
}
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function resolveDefaultMethod(options) {
|
|
410
|
+
const explicit = options.defaultMethod?.toUpperCase();
|
|
411
|
+
if (explicit && typeof options.httpHandlers[explicit] === "function") {
|
|
412
|
+
return explicit;
|
|
413
|
+
}
|
|
414
|
+
const preferredOrder = ["POST", "PUT", "PATCH", "GET", "DELETE", "OPTIONS", "HEAD"];
|
|
415
|
+
for (const method of preferredOrder) {
|
|
416
|
+
if (typeof options.httpHandlers[method] === "function") {
|
|
417
|
+
return method;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const available = Object.keys(options.httpHandlers).filter(
|
|
421
|
+
(method) => typeof options.httpHandlers[method] === "function"
|
|
422
|
+
);
|
|
423
|
+
if (available.length > 0) {
|
|
424
|
+
return available[0];
|
|
425
|
+
}
|
|
426
|
+
throw new Error(`No HTTP handlers available for tool "${options.name}"`);
|
|
427
|
+
}
|
|
428
|
+
function ensureSchema(schema) {
|
|
429
|
+
if (!schema) {
|
|
430
|
+
return void 0;
|
|
431
|
+
}
|
|
432
|
+
if (schema instanceof z.ZodType) {
|
|
433
|
+
return schema;
|
|
434
|
+
}
|
|
435
|
+
if (typeof schema?.parse === "function") {
|
|
436
|
+
return schema;
|
|
437
|
+
}
|
|
438
|
+
throw new Error("MCP adapter requires a valid Zod schema to validate arguments");
|
|
439
|
+
}
|
|
440
|
+
function buildRequest(name, method, params) {
|
|
441
|
+
const url = new URL(`https://opentool.local/${encodeURIComponent(name)}`);
|
|
442
|
+
const headers = new Headers({
|
|
443
|
+
"x-opentool-invocation": "mcp",
|
|
444
|
+
"x-opentool-tool": name
|
|
445
|
+
});
|
|
446
|
+
if (method === "GET" || method === "HEAD") {
|
|
447
|
+
if (params && typeof params === "object") {
|
|
448
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
449
|
+
if (value == null) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
url.searchParams.set(key, String(value));
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return new Request(url, { method, headers });
|
|
456
|
+
}
|
|
457
|
+
headers.set("Content-Type", "application/json");
|
|
458
|
+
const init = { method, headers };
|
|
459
|
+
if (params != null) {
|
|
460
|
+
init.body = JSON.stringify(params);
|
|
461
|
+
}
|
|
462
|
+
return new Request(url, init);
|
|
463
|
+
}
|
|
464
|
+
async function responseToToolResponse(response) {
|
|
465
|
+
const statusIsError = response.status >= 400;
|
|
466
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
467
|
+
const text = await response.text();
|
|
468
|
+
if (contentType.includes("application/json")) {
|
|
469
|
+
try {
|
|
470
|
+
const payload = text ? JSON.parse(text) : {};
|
|
471
|
+
if (payload && typeof payload === "object" && Array.isArray(payload.content)) {
|
|
472
|
+
return {
|
|
473
|
+
content: payload.content,
|
|
474
|
+
isError: payload.isError ?? statusIsError
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
479
|
+
isError: statusIsError
|
|
480
|
+
};
|
|
481
|
+
} catch {
|
|
482
|
+
return {
|
|
483
|
+
content: [{ type: "text", text }],
|
|
484
|
+
isError: statusIsError
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!text) {
|
|
489
|
+
return {
|
|
490
|
+
content: [],
|
|
491
|
+
isError: statusIsError
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
content: [{ type: "text", text }],
|
|
496
|
+
isError: statusIsError
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/types/index.ts
|
|
501
|
+
var HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
|
|
502
|
+
function resolveTsconfig(projectRoot) {
|
|
503
|
+
const candidate = path4.join(projectRoot, "tsconfig.json");
|
|
504
|
+
if (fs3.existsSync(candidate)) {
|
|
505
|
+
return candidate;
|
|
506
|
+
}
|
|
507
|
+
return void 0;
|
|
508
|
+
}
|
|
509
|
+
async function transpileWithEsbuild(options) {
|
|
510
|
+
if (options.entryPoints.length === 0) {
|
|
511
|
+
throw new Error("No entry points provided for esbuild transpilation");
|
|
512
|
+
}
|
|
513
|
+
const projectRoot = options.projectRoot;
|
|
514
|
+
const tempBase = options.outDir ?? fs3.mkdtempSync(path4.join(tmpdir(), "opentool-"));
|
|
515
|
+
if (!fs3.existsSync(tempBase)) {
|
|
516
|
+
fs3.mkdirSync(tempBase, { recursive: true });
|
|
517
|
+
}
|
|
518
|
+
const tsconfig = resolveTsconfig(projectRoot);
|
|
519
|
+
const buildOptions = {
|
|
520
|
+
entryPoints: options.entryPoints,
|
|
521
|
+
outdir: tempBase,
|
|
522
|
+
bundle: options.bundle ?? false,
|
|
523
|
+
format: options.format,
|
|
524
|
+
platform: "node",
|
|
525
|
+
target: "node20",
|
|
526
|
+
logLevel: options.logLevel ?? "warning",
|
|
527
|
+
sourcesContent: false,
|
|
528
|
+
sourcemap: false,
|
|
529
|
+
loader: {
|
|
530
|
+
".ts": "ts",
|
|
531
|
+
".tsx": "tsx",
|
|
532
|
+
".cts": "ts",
|
|
533
|
+
".mts": "ts",
|
|
534
|
+
".js": "js",
|
|
535
|
+
".jsx": "jsx",
|
|
536
|
+
".mjs": "js",
|
|
537
|
+
".cjs": "js",
|
|
538
|
+
".json": "json"
|
|
539
|
+
},
|
|
540
|
+
metafile: options.metafile ?? false,
|
|
541
|
+
allowOverwrite: true,
|
|
542
|
+
absWorkingDir: projectRoot
|
|
543
|
+
};
|
|
544
|
+
if (options.external && options.external.length > 0) {
|
|
545
|
+
buildOptions.external = options.external;
|
|
546
|
+
}
|
|
547
|
+
if (options.nodePaths && options.nodePaths.length > 0) {
|
|
548
|
+
buildOptions.nodePaths = options.nodePaths;
|
|
549
|
+
}
|
|
550
|
+
if (options.outBase) {
|
|
551
|
+
buildOptions.outbase = options.outBase;
|
|
552
|
+
}
|
|
553
|
+
if (!buildOptions.bundle) {
|
|
554
|
+
buildOptions.packages = "external";
|
|
555
|
+
}
|
|
556
|
+
if (tsconfig) {
|
|
557
|
+
buildOptions.tsconfig = tsconfig;
|
|
558
|
+
}
|
|
559
|
+
await build(buildOptions);
|
|
560
|
+
if (options.format === "esm") {
|
|
561
|
+
const packageJsonPath = path4.join(tempBase, "package.json");
|
|
562
|
+
if (!fs3.existsSync(packageJsonPath)) {
|
|
563
|
+
fs3.writeFileSync(packageJsonPath, JSON.stringify({ type: "module" }), "utf8");
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const cleanup = () => {
|
|
567
|
+
if (options.outDir) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
fs3.rmSync(tempBase, { recursive: true, force: true });
|
|
571
|
+
};
|
|
572
|
+
return { outDir: tempBase, cleanup };
|
|
573
|
+
}
|
|
574
|
+
createRequire(
|
|
575
|
+
typeof __filename !== "undefined" ? __filename : import.meta.url
|
|
576
|
+
);
|
|
577
|
+
function resolveCompiledPath(outDir, originalFile, extension = ".js") {
|
|
578
|
+
const baseName = path4.basename(originalFile).replace(/\.[^.]+$/, "");
|
|
579
|
+
return path4.join(outDir, `${baseName}${extension}`);
|
|
580
|
+
}
|
|
581
|
+
async function importFresh(modulePath) {
|
|
582
|
+
const fileUrl = pathToFileURL(modulePath).href;
|
|
583
|
+
const cacheBuster = `t=${Date.now()}-${Math.random()}`;
|
|
584
|
+
const separator = fileUrl.includes("?") ? "&" : "?";
|
|
585
|
+
return import(`${fileUrl}${separator}${cacheBuster}`);
|
|
586
|
+
}
|
|
587
|
+
var METADATA_SPEC_VERSION = "1.1.0";
|
|
588
|
+
var McpAnnotationsSchema = z.object({
|
|
589
|
+
title: z.string().optional(),
|
|
590
|
+
readOnlyHint: z.boolean().optional(),
|
|
591
|
+
destructiveHint: z.boolean().optional(),
|
|
592
|
+
idempotentHint: z.boolean().optional(),
|
|
593
|
+
openWorldHint: z.boolean().optional(),
|
|
594
|
+
requiresPayment: z.boolean().optional()
|
|
595
|
+
}).strict();
|
|
596
|
+
var X402PaymentSchema = z.object({
|
|
597
|
+
definition: z.object({
|
|
598
|
+
amount: z.string(),
|
|
599
|
+
currency: z.object({
|
|
600
|
+
code: z.string(),
|
|
601
|
+
symbol: z.string(),
|
|
602
|
+
decimals: z.number()
|
|
603
|
+
}),
|
|
604
|
+
asset: z.object({
|
|
605
|
+
symbol: z.string(),
|
|
606
|
+
network: z.string(),
|
|
607
|
+
address: z.string(),
|
|
608
|
+
decimals: z.number()
|
|
609
|
+
}),
|
|
610
|
+
payTo: z.string(),
|
|
611
|
+
resource: z.string().optional(),
|
|
612
|
+
description: z.string().optional(),
|
|
613
|
+
scheme: z.string(),
|
|
614
|
+
network: z.string(),
|
|
615
|
+
facilitator: z.object({
|
|
616
|
+
url: z.string(),
|
|
617
|
+
verifyPath: z.string().optional(),
|
|
618
|
+
settlePath: z.string().optional(),
|
|
619
|
+
apiKeyHeader: z.string().optional()
|
|
620
|
+
}),
|
|
621
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
622
|
+
}),
|
|
623
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
624
|
+
}).passthrough();
|
|
625
|
+
var PaymentConfigSchema = z.union([X402PaymentSchema, z.record(z.string(), z.unknown())]);
|
|
626
|
+
var DiscoveryMetadataSchema = z.object({
|
|
627
|
+
keywords: z.array(z.string()).optional(),
|
|
628
|
+
category: z.string().optional(),
|
|
629
|
+
useCases: z.array(z.string()).optional(),
|
|
630
|
+
capabilities: z.array(z.string()).optional(),
|
|
631
|
+
requirements: z.record(z.string(), z.any()).optional(),
|
|
632
|
+
compatibility: z.record(z.string(), z.any()).optional(),
|
|
633
|
+
documentation: z.union([z.string(), z.array(z.string())]).optional()
|
|
634
|
+
}).catchall(z.any());
|
|
635
|
+
var ToolCategorySchema = z.enum(["strategy", "tracker", "orchestrator"]);
|
|
636
|
+
z.object({
|
|
637
|
+
name: z.string().optional(),
|
|
638
|
+
description: z.string().optional(),
|
|
639
|
+
annotations: McpAnnotationsSchema.optional(),
|
|
640
|
+
payment: PaymentConfigSchema.optional(),
|
|
641
|
+
discovery: DiscoveryMetadataSchema.optional(),
|
|
642
|
+
chains: z.array(z.union([z.string(), z.number()])).optional()
|
|
643
|
+
}).catchall(z.any());
|
|
644
|
+
var ToolSchema = z.object({
|
|
645
|
+
name: z.string(),
|
|
646
|
+
description: z.string(),
|
|
647
|
+
inputSchema: z.any(),
|
|
648
|
+
annotations: McpAnnotationsSchema.optional(),
|
|
649
|
+
payment: PaymentConfigSchema.optional(),
|
|
650
|
+
discovery: DiscoveryMetadataSchema.optional(),
|
|
651
|
+
chains: z.array(z.union([z.string(), z.number()])).optional(),
|
|
652
|
+
notifyEmail: z.boolean().optional(),
|
|
653
|
+
category: ToolCategorySchema.optional()
|
|
654
|
+
}).strict();
|
|
655
|
+
z.object({
|
|
656
|
+
metadataSpecVersion: z.string().optional(),
|
|
657
|
+
name: z.string().optional(),
|
|
658
|
+
displayName: z.string().optional(),
|
|
659
|
+
version: z.string().optional(),
|
|
660
|
+
description: z.string().optional(),
|
|
661
|
+
author: z.string().optional(),
|
|
662
|
+
repository: z.string().optional(),
|
|
663
|
+
website: z.string().optional(),
|
|
664
|
+
category: z.string().optional(),
|
|
665
|
+
categories: z.array(z.string()).optional(),
|
|
666
|
+
termsOfService: z.string().optional(),
|
|
667
|
+
mcpUrl: z.string().optional(),
|
|
668
|
+
payment: PaymentConfigSchema.optional(),
|
|
669
|
+
discovery: DiscoveryMetadataSchema.optional(),
|
|
670
|
+
promptExamples: z.array(z.string()).optional(),
|
|
671
|
+
iconPath: z.string().optional(),
|
|
672
|
+
videoPath: z.string().optional(),
|
|
673
|
+
image: z.string().optional(),
|
|
674
|
+
animation_url: z.string().optional(),
|
|
675
|
+
keywords: z.array(z.string()).optional(),
|
|
676
|
+
useCases: z.array(z.string()).optional(),
|
|
677
|
+
capabilities: z.array(z.string()).optional(),
|
|
678
|
+
requirements: z.record(z.string(), z.any()).optional(),
|
|
679
|
+
compatibility: z.record(z.string(), z.any()).optional(),
|
|
680
|
+
chains: z.array(z.union([z.string(), z.number()])).optional()
|
|
681
|
+
}).catchall(z.any());
|
|
682
|
+
z.object({
|
|
683
|
+
metadataSpecVersion: z.string().default(METADATA_SPEC_VERSION),
|
|
684
|
+
name: z.string(),
|
|
685
|
+
displayName: z.string(),
|
|
686
|
+
version: z.string(),
|
|
687
|
+
description: z.string().optional(),
|
|
688
|
+
author: z.string().optional(),
|
|
689
|
+
repository: z.string().optional(),
|
|
690
|
+
website: z.string().optional(),
|
|
691
|
+
category: z.string(),
|
|
692
|
+
termsOfService: z.string().optional(),
|
|
693
|
+
mcpUrl: z.string().optional(),
|
|
694
|
+
payment: PaymentConfigSchema.optional(),
|
|
695
|
+
tools: z.array(ToolSchema).min(1),
|
|
696
|
+
discovery: DiscoveryMetadataSchema.optional(),
|
|
697
|
+
promptExamples: z.array(z.string()).optional(),
|
|
698
|
+
iconPath: z.string().optional(),
|
|
699
|
+
videoPath: z.string().optional(),
|
|
700
|
+
image: z.string().optional(),
|
|
701
|
+
animation_url: z.string().optional(),
|
|
702
|
+
chains: z.array(z.union([z.string(), z.number()])).optional()
|
|
703
|
+
}).strict();
|
|
704
|
+
|
|
705
|
+
// src/utils/schedule.ts
|
|
706
|
+
var CRON_WRAPPED_REGEX = /^cron\((.*)\)$/i;
|
|
707
|
+
var CRON_TOKEN_REGEX = /^[A-Za-z0-9*?/,\-#L]+$/;
|
|
708
|
+
function normalizeScheduleExpression(raw, context) {
|
|
709
|
+
const value = raw?.trim();
|
|
710
|
+
if (!value) {
|
|
711
|
+
throw new Error(`${context}: profile.schedule.cron must be a non-empty string`);
|
|
712
|
+
}
|
|
713
|
+
const cronBody = extractCronBody(value);
|
|
714
|
+
const cronFields = cronBody.trim().split(/\s+/).filter(Boolean);
|
|
715
|
+
if (cronFields.length !== 5 && cronFields.length !== 6) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
`${context}: cron expression must have 5 or 6 fields (got ${cronFields.length})`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
validateCronTokens(cronFields, context);
|
|
721
|
+
return {
|
|
722
|
+
type: "cron",
|
|
723
|
+
expression: cronFields.join(" ")
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function extractCronBody(value) {
|
|
727
|
+
const cronMatch = CRON_WRAPPED_REGEX.exec(value);
|
|
728
|
+
if (cronMatch) {
|
|
729
|
+
return (cronMatch[1] ?? "").trim();
|
|
730
|
+
}
|
|
731
|
+
return value;
|
|
732
|
+
}
|
|
733
|
+
function validateCronTokens(fields, context) {
|
|
734
|
+
fields.forEach((token, idx) => {
|
|
735
|
+
if (!CRON_TOKEN_REGEX.test(token)) {
|
|
736
|
+
throw new Error(`${context}: invalid cron token "${token}" at position ${idx + 1}`);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/cli/validate.ts
|
|
742
|
+
var SUPPORTED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
743
|
+
var OPENTOOL_ROOT = path4.resolve(path4.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
744
|
+
var OPENTOOL_NODE_MODULES = path4.join(OPENTOOL_ROOT, "node_modules");
|
|
745
|
+
var MIN_TEMPLATE_CONFIG_VERSION = 2;
|
|
746
|
+
var TEMPLATE_PREVIEW_TITLE_MAX = 80;
|
|
747
|
+
var TEMPLATE_PREVIEW_SUBTITLE_MAX = 120;
|
|
748
|
+
var TEMPLATE_PREVIEW_DESCRIPTION_MAX = 1200;
|
|
749
|
+
var TEMPLATE_PREVIEW_MIN_LINES = 3;
|
|
750
|
+
var TEMPLATE_PREVIEW_MAX_LINES = 8;
|
|
751
|
+
function normalizeTemplateConfigVersion(value) {
|
|
752
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
753
|
+
return value;
|
|
754
|
+
}
|
|
755
|
+
if (typeof value !== "string") {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
const trimmed = value.trim();
|
|
759
|
+
if (!trimmed) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
if (/^\d+(?:\.\d+)?$/.test(trimmed)) {
|
|
763
|
+
const numeric = Number.parseFloat(trimmed);
|
|
764
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
765
|
+
}
|
|
766
|
+
const majorMatch = /^v?(\d+)(?:\..*)?$/i.exec(trimmed);
|
|
767
|
+
if (!majorMatch) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
const major = Number.parseInt(majorMatch[1], 10);
|
|
771
|
+
return Number.isFinite(major) ? major : null;
|
|
772
|
+
}
|
|
773
|
+
function parseNonEmptyString(value, fieldPath, opts = {}) {
|
|
774
|
+
const { max, required = false } = opts;
|
|
775
|
+
if (value == null) {
|
|
776
|
+
if (required) {
|
|
777
|
+
throw new Error(`${fieldPath} is required and must be a non-empty string.`);
|
|
778
|
+
}
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
if (typeof value !== "string") {
|
|
782
|
+
throw new Error(`${fieldPath} must be a string.`);
|
|
783
|
+
}
|
|
784
|
+
const trimmed = value.trim();
|
|
785
|
+
if (!trimmed) {
|
|
786
|
+
throw new Error(`${fieldPath} must be a non-empty string.`);
|
|
787
|
+
}
|
|
788
|
+
if (typeof max === "number" && trimmed.length > max) {
|
|
789
|
+
throw new Error(`${fieldPath} must be <= ${max} characters.`);
|
|
790
|
+
}
|
|
791
|
+
return trimmed;
|
|
792
|
+
}
|
|
793
|
+
function normalizeTemplatePreview(value, file, toolName, requirePreview) {
|
|
794
|
+
const pathPrefix = `${file}: profile.templatePreview`;
|
|
795
|
+
if (value == null) {
|
|
796
|
+
if (requirePreview) {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`${pathPrefix} is required for strategy tools and must define subtitle + description.`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
804
|
+
throw new Error(`${pathPrefix} must be an object.`);
|
|
805
|
+
}
|
|
806
|
+
const record = value;
|
|
807
|
+
const title = parseNonEmptyString(record.title, `${pathPrefix}.title`, {
|
|
808
|
+
max: TEMPLATE_PREVIEW_TITLE_MAX
|
|
809
|
+
}) ?? toolName;
|
|
810
|
+
const subtitle = parseNonEmptyString(record.subtitle, `${pathPrefix}.subtitle`, {
|
|
811
|
+
required: true,
|
|
812
|
+
max: TEMPLATE_PREVIEW_SUBTITLE_MAX
|
|
813
|
+
});
|
|
814
|
+
const description = parseNonEmptyString(record.description, `${pathPrefix}.description`, {
|
|
815
|
+
required: true,
|
|
816
|
+
max: TEMPLATE_PREVIEW_DESCRIPTION_MAX
|
|
817
|
+
});
|
|
818
|
+
const descriptionLineCount = description.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).length;
|
|
819
|
+
if (descriptionLineCount < TEMPLATE_PREVIEW_MIN_LINES || descriptionLineCount > TEMPLATE_PREVIEW_MAX_LINES) {
|
|
820
|
+
throw new Error(
|
|
821
|
+
`${pathPrefix}.description must contain ${TEMPLATE_PREVIEW_MIN_LINES}-${TEMPLATE_PREVIEW_MAX_LINES} non-empty lines (target ~5 lines).`
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
title,
|
|
826
|
+
subtitle,
|
|
827
|
+
description
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
async function loadAndValidateTools(toolsDir, options = {}) {
|
|
831
|
+
const files = fs3.readdirSync(toolsDir).filter((file) => SUPPORTED_EXTENSIONS.includes(path4.extname(file)));
|
|
832
|
+
if (files.length === 0) {
|
|
833
|
+
return [];
|
|
834
|
+
}
|
|
835
|
+
const projectRoot = options.projectRoot ?? path4.dirname(toolsDir);
|
|
836
|
+
const tempDir = path4.join(toolsDir, ".opentool-temp");
|
|
837
|
+
if (fs3.existsSync(tempDir)) {
|
|
838
|
+
fs3.rmSync(tempDir, { recursive: true, force: true });
|
|
839
|
+
}
|
|
840
|
+
const kebabCase = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z]+$/;
|
|
841
|
+
for (const f of files) {
|
|
842
|
+
if (!kebabCase.test(f)) {
|
|
843
|
+
throw new Error(`Tool filename must be kebab-case: ${f}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const entryPoints = files.map((file) => path4.join(toolsDir, file));
|
|
847
|
+
const fallbackNodePaths = [OPENTOOL_NODE_MODULES].filter((dir) => fs3.existsSync(dir));
|
|
848
|
+
const { outDir, cleanup } = await transpileWithEsbuild({
|
|
849
|
+
entryPoints,
|
|
850
|
+
projectRoot,
|
|
851
|
+
format: "esm",
|
|
852
|
+
outDir: tempDir,
|
|
853
|
+
bundle: true,
|
|
854
|
+
external: ["opentool", "opentool/*"],
|
|
855
|
+
...fallbackNodePaths.length > 0 ? { nodePaths: fallbackNodePaths } : {}
|
|
856
|
+
});
|
|
857
|
+
const tools = [];
|
|
858
|
+
try {
|
|
859
|
+
ensureLocalRuntimeLinks(tempDir);
|
|
860
|
+
for (const file of files) {
|
|
861
|
+
const compiledPath = resolveCompiledPath(outDir, file);
|
|
862
|
+
if (!fs3.existsSync(compiledPath)) {
|
|
863
|
+
throw new Error(`Failed to compile ${file}`);
|
|
864
|
+
}
|
|
865
|
+
const moduleExports = await importFresh(compiledPath);
|
|
866
|
+
const toolModule = extractToolModule(moduleExports, file);
|
|
867
|
+
const schema = ensureZodSchema(toolModule.schema, file);
|
|
868
|
+
const paymentExport = toolModule.payment;
|
|
869
|
+
const toolName = toolModule.metadata?.name ?? toolModule.metadata?.title ?? toBaseName(file);
|
|
870
|
+
const inputSchemaRaw = schema ? toJsonSchema(toolName, schema) : void 0;
|
|
871
|
+
const inputSchema = normalizeInputSchema(inputSchemaRaw);
|
|
872
|
+
const httpHandlersRaw = collectHttpHandlers(toolModule, file);
|
|
873
|
+
const hasGET = typeof toolModule.GET === "function";
|
|
874
|
+
const hasPOST = typeof toolModule.POST === "function";
|
|
875
|
+
const otherMethods = HTTP_METHODS.filter((m) => m !== "GET" && m !== "POST").filter(
|
|
876
|
+
(m) => typeof toolModule[m] === "function"
|
|
877
|
+
);
|
|
878
|
+
if (otherMethods.length > 0) {
|
|
879
|
+
throw new Error(
|
|
880
|
+
`${file} must not export ${otherMethods.join(", ")}. Only one of GET or POST is allowed.`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
if (hasGET === hasPOST) {
|
|
884
|
+
throw new Error(`${file}: export exactly one of GET or POST`);
|
|
885
|
+
}
|
|
886
|
+
let normalizedSchedule = null;
|
|
887
|
+
const profileRaw = toolModule?.profile && typeof toolModule.profile === "object" ? toolModule.profile : null;
|
|
888
|
+
const schedule = profileRaw?.schedule ?? null;
|
|
889
|
+
const profileNotifyEmail = typeof profileRaw?.notifyEmail === "boolean" ? profileRaw.notifyEmail : void 0;
|
|
890
|
+
const allowedProfileCategories = ["strategy", "tracker", "orchestrator"];
|
|
891
|
+
const profileCategoryCandidate = typeof profileRaw?.category === "string" ? profileRaw.category : void 0;
|
|
892
|
+
let profileCategoryRaw;
|
|
893
|
+
if (profileCategoryCandidate !== void 0) {
|
|
894
|
+
const isAllowed = allowedProfileCategories.includes(
|
|
895
|
+
profileCategoryCandidate
|
|
896
|
+
);
|
|
897
|
+
if (!isAllowed) {
|
|
898
|
+
throw new Error(
|
|
899
|
+
`${file}: profile.category must be one of ${allowedProfileCategories.join(", ")}`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
profileCategoryRaw = profileCategoryCandidate;
|
|
903
|
+
}
|
|
904
|
+
const profileAssetsRaw = profileRaw?.assets;
|
|
905
|
+
if (profileAssetsRaw !== void 0) {
|
|
906
|
+
if (!Array.isArray(profileAssetsRaw)) {
|
|
907
|
+
throw new Error(`${file}: profile.assets must be an array.`);
|
|
908
|
+
}
|
|
909
|
+
profileAssetsRaw.forEach((entry, index) => {
|
|
910
|
+
if (!entry || typeof entry !== "object") {
|
|
911
|
+
throw new Error(`${file}: profile.assets[${index}] must be an object.`);
|
|
912
|
+
}
|
|
913
|
+
const record = entry;
|
|
914
|
+
const venue = typeof record.venue === "string" ? record.venue.trim() : "";
|
|
915
|
+
if (!venue) {
|
|
916
|
+
throw new Error(`${file}: profile.assets[${index}].venue must be a non-empty string.`);
|
|
917
|
+
}
|
|
918
|
+
const chain = record.chain;
|
|
919
|
+
if (typeof chain !== "string" && typeof chain !== "number") {
|
|
920
|
+
throw new Error(`${file}: profile.assets[${index}].chain must be a string or number.`);
|
|
921
|
+
}
|
|
922
|
+
const symbols = record.assetSymbols;
|
|
923
|
+
if (!Array.isArray(symbols) || symbols.length === 0) {
|
|
924
|
+
throw new Error(
|
|
925
|
+
`${file}: profile.assets[${index}].assetSymbols must be a non-empty array.`
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
const invalidSymbol = symbols.find(
|
|
929
|
+
(symbol) => typeof symbol !== "string" || symbol.trim().length === 0
|
|
930
|
+
);
|
|
931
|
+
if (invalidSymbol !== void 0) {
|
|
932
|
+
throw new Error(
|
|
933
|
+
`${file}: profile.assets[${index}].assetSymbols must be non-empty strings.`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
const walletAddress = record.walletAddress;
|
|
937
|
+
if (walletAddress !== void 0 && (typeof walletAddress !== "string" || walletAddress.trim().length === 0)) {
|
|
938
|
+
throw new Error(
|
|
939
|
+
`${file}: profile.assets[${index}].walletAddress must be a non-empty string when provided.`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
const pair = record.pair;
|
|
943
|
+
if (pair !== void 0 && (typeof pair !== "string" || pair.trim().length === 0)) {
|
|
944
|
+
throw new Error(
|
|
945
|
+
`${file}: profile.assets[${index}].pair must be a non-empty string when provided.`
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
const leverage = record.leverage;
|
|
949
|
+
if (leverage !== void 0 && (typeof leverage !== "number" || !Number.isFinite(leverage) || leverage <= 0)) {
|
|
950
|
+
throw new Error(
|
|
951
|
+
`${file}: profile.assets[${index}].leverage must be a positive number when provided.`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
const templateConfigRaw = profileRaw?.templateConfig;
|
|
957
|
+
if (templateConfigRaw !== void 0) {
|
|
958
|
+
if (!templateConfigRaw || typeof templateConfigRaw !== "object") {
|
|
959
|
+
throw new Error(`${file}: profile.templateConfig must be an object.`);
|
|
960
|
+
}
|
|
961
|
+
const record = templateConfigRaw;
|
|
962
|
+
const version = record.version;
|
|
963
|
+
const normalizedTemplateConfigVersion = normalizeTemplateConfigVersion(version);
|
|
964
|
+
if (normalizedTemplateConfigVersion === null) {
|
|
965
|
+
throw new Error(
|
|
966
|
+
`${file}: profile.templateConfig.version must be a numeric string or number.`
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
if (normalizedTemplateConfigVersion < MIN_TEMPLATE_CONFIG_VERSION) {
|
|
970
|
+
throw new Error(
|
|
971
|
+
`${file}: profile.templateConfig.version must be >= ${MIN_TEMPLATE_CONFIG_VERSION}.`
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
const schema2 = record.schema;
|
|
975
|
+
if (schema2 !== void 0 && (!schema2 || typeof schema2 !== "object" || Array.isArray(schema2))) {
|
|
976
|
+
throw new Error(
|
|
977
|
+
`${file}: profile.templateConfig.schema must be an object when provided.`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
const defaults = record.defaults;
|
|
981
|
+
if (defaults !== void 0 && (!defaults || typeof defaults !== "object" || Array.isArray(defaults))) {
|
|
982
|
+
throw new Error(
|
|
983
|
+
`${file}: profile.templateConfig.defaults must be an object when provided.`
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
const envVar = record.envVar;
|
|
987
|
+
if (envVar !== void 0 && (typeof envVar !== "string" || envVar.trim().length === 0)) {
|
|
988
|
+
throw new Error(
|
|
989
|
+
`${file}: profile.templateConfig.envVar must be a non-empty string when provided.`
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const normalizedTemplatePreview = normalizeTemplatePreview(
|
|
994
|
+
profileRaw?.templatePreview,
|
|
995
|
+
file,
|
|
996
|
+
toolName,
|
|
997
|
+
profileCategoryRaw === "strategy"
|
|
998
|
+
);
|
|
999
|
+
const normalizedProfile = profileRaw && normalizedTemplatePreview ? { ...profileRaw, templatePreview: normalizedTemplatePreview } : profileRaw;
|
|
1000
|
+
if (hasGET && schedule && typeof schedule.cron === "string" && schedule.cron.trim().length > 0) {
|
|
1001
|
+
normalizedSchedule = normalizeScheduleExpression(schedule.cron, file);
|
|
1002
|
+
if (typeof schedule.enabled === "boolean") {
|
|
1003
|
+
normalizedSchedule.authoredEnabled = schedule.enabled;
|
|
1004
|
+
}
|
|
1005
|
+
if (typeof schedule.notifyEmail === "boolean") {
|
|
1006
|
+
normalizedSchedule.notifyEmail = schedule.notifyEmail;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (hasPOST) {
|
|
1010
|
+
if (!schema) {
|
|
1011
|
+
throw new Error(`${file}: POST tools must export a Zod schema as 'schema'`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const httpHandlers = [...httpHandlersRaw];
|
|
1015
|
+
if (httpHandlers.length === 0) {
|
|
1016
|
+
throw new Error(`${file} must export at least one HTTP handler (e.g. POST)`);
|
|
1017
|
+
}
|
|
1018
|
+
if (paymentExport) {
|
|
1019
|
+
for (let index = 0; index < httpHandlers.length; index += 1) {
|
|
1020
|
+
const entry = httpHandlers[index];
|
|
1021
|
+
httpHandlers[index] = {
|
|
1022
|
+
...entry,
|
|
1023
|
+
handler: withX402Payment(entry.handler, paymentExport)
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
const httpHandlerMap = toHttpHandlerMap(httpHandlers);
|
|
1028
|
+
const defaultMethod = typeof toolModule.mcp?.defaultMethod === "string" ? toolModule.mcp.defaultMethod : void 0;
|
|
1029
|
+
const adapter = createMcpAdapter({
|
|
1030
|
+
name: toolName,
|
|
1031
|
+
httpHandlers: httpHandlerMap,
|
|
1032
|
+
...defaultMethod ? { defaultMethod } : {},
|
|
1033
|
+
...schema ? { schema } : {}
|
|
1034
|
+
});
|
|
1035
|
+
let metadataOverrides = toolModule.metadata ?? null;
|
|
1036
|
+
if (paymentExport) {
|
|
1037
|
+
if (metadataOverrides) {
|
|
1038
|
+
metadataOverrides = {
|
|
1039
|
+
...metadataOverrides,
|
|
1040
|
+
payment: metadataOverrides.payment ?? paymentExport,
|
|
1041
|
+
annotations: {
|
|
1042
|
+
...metadataOverrides.annotations,
|
|
1043
|
+
requiresPayment: metadataOverrides.annotations?.requiresPayment ?? true
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
} else {
|
|
1047
|
+
metadataOverrides = {
|
|
1048
|
+
payment: paymentExport,
|
|
1049
|
+
annotations: { requiresPayment: true }
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const tool = {
|
|
1054
|
+
schema: schema ?? void 0,
|
|
1055
|
+
inputSchema,
|
|
1056
|
+
metadata: metadataOverrides,
|
|
1057
|
+
httpHandlers,
|
|
1058
|
+
mcpConfig: normalizeMcpConfig(toolModule.mcp, file),
|
|
1059
|
+
filename: toBaseName(file),
|
|
1060
|
+
sourcePath: path4.join(toolsDir, file),
|
|
1061
|
+
handler: async (params) => adapter(params),
|
|
1062
|
+
payment: paymentExport ?? null,
|
|
1063
|
+
schedule: normalizedSchedule,
|
|
1064
|
+
profile: normalizedProfile,
|
|
1065
|
+
...profileNotifyEmail !== void 0 ? { notifyEmail: profileNotifyEmail } : {},
|
|
1066
|
+
profileDescription: typeof profileRaw?.description === "string" ? profileRaw.description : null,
|
|
1067
|
+
...profileCategoryRaw ? { profileCategory: profileCategoryRaw } : {}
|
|
1068
|
+
};
|
|
1069
|
+
tools.push(tool);
|
|
1070
|
+
}
|
|
1071
|
+
} finally {
|
|
1072
|
+
cleanup();
|
|
1073
|
+
if (fs3.existsSync(tempDir)) {
|
|
1074
|
+
fs3.rmSync(tempDir, { recursive: true, force: true });
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return tools;
|
|
1078
|
+
}
|
|
1079
|
+
function ensureLocalRuntimeLinks(tempDir) {
|
|
1080
|
+
const nodeModulesDir = path4.join(tempDir, "node_modules");
|
|
1081
|
+
fs3.mkdirSync(nodeModulesDir, { recursive: true });
|
|
1082
|
+
const packageLinks = [
|
|
1083
|
+
{ name: "opentool", target: OPENTOOL_ROOT },
|
|
1084
|
+
{ name: "zod", target: path4.join(OPENTOOL_NODE_MODULES, "zod") }
|
|
1085
|
+
];
|
|
1086
|
+
for (const { name, target } of packageLinks) {
|
|
1087
|
+
if (!fs3.existsSync(target)) {
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
const linkPath = path4.join(nodeModulesDir, name);
|
|
1091
|
+
if (fs3.existsSync(linkPath)) {
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
fs3.symlinkSync(target, linkPath, "junction");
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
function extractToolModule(exportsObject, filename) {
|
|
1098
|
+
const candidates = [exportsObject, exportsObject?.default];
|
|
1099
|
+
for (const candidate of candidates) {
|
|
1100
|
+
if (candidate && typeof candidate === "object") {
|
|
1101
|
+
const hasSchema = candidate.schema && typeof candidate.schema === "object";
|
|
1102
|
+
const hasHttp = HTTP_METHODS.some((method) => typeof candidate[method] === "function");
|
|
1103
|
+
if (hasSchema || hasHttp) {
|
|
1104
|
+
return candidate;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
throw new Error(
|
|
1109
|
+
`${filename} must export a tool definition. Expected a Zod schema plus HTTP handlers (export async function POST).`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
function toJsonSchema(name, schema) {
|
|
1113
|
+
if (!schema) {
|
|
1114
|
+
return void 0;
|
|
1115
|
+
}
|
|
1116
|
+
try {
|
|
1117
|
+
return zodToJsonSchema(schema, {
|
|
1118
|
+
name: `${name}Schema`,
|
|
1119
|
+
target: "jsonSchema7",
|
|
1120
|
+
$refStrategy: "none"
|
|
1121
|
+
});
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
throw new Error(`Failed to convert Zod schema for ${name}: ${error}`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
function toBaseName(file) {
|
|
1127
|
+
return file.replace(/\.[^.]+$/, "");
|
|
1128
|
+
}
|
|
1129
|
+
function ensureZodSchema(schemaCandidate, filename) {
|
|
1130
|
+
if (schemaCandidate == null) {
|
|
1131
|
+
return void 0;
|
|
1132
|
+
}
|
|
1133
|
+
if (schemaCandidate instanceof z.ZodType) {
|
|
1134
|
+
return schemaCandidate;
|
|
1135
|
+
}
|
|
1136
|
+
const schema = schemaCandidate;
|
|
1137
|
+
if (typeof schema?.parse !== "function") {
|
|
1138
|
+
throw new Error(`${filename} schema export must be a Zod schema (missing parse method)`);
|
|
1139
|
+
}
|
|
1140
|
+
return schema;
|
|
1141
|
+
}
|
|
1142
|
+
function collectHttpHandlers(module, filename) {
|
|
1143
|
+
const handlers = [];
|
|
1144
|
+
for (const method of HTTP_METHODS) {
|
|
1145
|
+
const handler = module?.[method];
|
|
1146
|
+
if (typeof handler === "function") {
|
|
1147
|
+
handlers.push({
|
|
1148
|
+
method,
|
|
1149
|
+
handler: async (request) => handler.call(module, request)
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
handlers.sort((a, b) => HTTP_METHODS.indexOf(a.method) - HTTP_METHODS.indexOf(b.method));
|
|
1154
|
+
const duplicates = findDuplicates(handlers.map((h) => h.method));
|
|
1155
|
+
if (duplicates.length > 0) {
|
|
1156
|
+
throw new Error(
|
|
1157
|
+
`${filename} exports multiple handlers for HTTP method(s): ${duplicates.join(", ")}`
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
return handlers;
|
|
1161
|
+
}
|
|
1162
|
+
function toHttpHandlerMap(handlers) {
|
|
1163
|
+
return handlers.reduce((acc, handler) => {
|
|
1164
|
+
acc[handler.method.toUpperCase()] = handler.handler;
|
|
1165
|
+
return acc;
|
|
1166
|
+
}, {});
|
|
1167
|
+
}
|
|
1168
|
+
function normalizeInputSchema(schema) {
|
|
1169
|
+
if (!schema || typeof schema !== "object") {
|
|
1170
|
+
return schema;
|
|
1171
|
+
}
|
|
1172
|
+
const clone = JSON.parse(JSON.stringify(schema));
|
|
1173
|
+
if (typeof clone.$ref === "string" && clone.$ref.startsWith("#/definitions/")) {
|
|
1174
|
+
const refName = clone.$ref.replace("#/definitions/", "");
|
|
1175
|
+
const definitions = clone.definitions;
|
|
1176
|
+
if (definitions && typeof definitions[refName] === "object") {
|
|
1177
|
+
return normalizeInputSchema(definitions[refName]);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
delete clone.$ref;
|
|
1181
|
+
delete clone.definitions;
|
|
1182
|
+
if (!("type" in clone)) {
|
|
1183
|
+
clone.type = "object";
|
|
1184
|
+
}
|
|
1185
|
+
return clone;
|
|
1186
|
+
}
|
|
1187
|
+
function normalizeMcpConfig(rawConfig, filename) {
|
|
1188
|
+
if (rawConfig == null) {
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1191
|
+
if (rawConfig === false) {
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
if (rawConfig === true) {
|
|
1195
|
+
return { enabled: true };
|
|
1196
|
+
}
|
|
1197
|
+
if (!isPlainObject(rawConfig)) {
|
|
1198
|
+
throw new Error(`${filename} export \\"mcp\\" must be an object with an enabled flag`);
|
|
1199
|
+
}
|
|
1200
|
+
const enabledRaw = rawConfig.enabled;
|
|
1201
|
+
if (enabledRaw === false) {
|
|
1202
|
+
return null;
|
|
1203
|
+
}
|
|
1204
|
+
if (enabledRaw !== true) {
|
|
1205
|
+
throw new Error(`${filename} mcp.enabled must be explicitly set to true to opt-in to MCP`);
|
|
1206
|
+
}
|
|
1207
|
+
const modeRaw = rawConfig.mode;
|
|
1208
|
+
let mode;
|
|
1209
|
+
if (typeof modeRaw === "string") {
|
|
1210
|
+
const normalized = modeRaw.toLowerCase();
|
|
1211
|
+
if (["stdio", "lambda", "dual"].includes(normalized)) {
|
|
1212
|
+
mode = normalized;
|
|
1213
|
+
} else {
|
|
1214
|
+
throw new Error(
|
|
1215
|
+
`${filename} mcp.mode must be one of "stdio", "lambda", or "dual" if specified`
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const defaultMethodRaw = rawConfig.defaultMethod;
|
|
1220
|
+
const defaultMethod = typeof defaultMethodRaw === "string" ? defaultMethodRaw.toUpperCase() : void 0;
|
|
1221
|
+
const overridesRaw = rawConfig.metadataOverrides;
|
|
1222
|
+
const metadataOverrides = isPlainObject(overridesRaw) ? overridesRaw : void 0;
|
|
1223
|
+
const config = {
|
|
1224
|
+
enabled: true
|
|
1225
|
+
};
|
|
1226
|
+
if (mode) {
|
|
1227
|
+
config.mode = mode;
|
|
1228
|
+
}
|
|
1229
|
+
if (defaultMethod) {
|
|
1230
|
+
config.defaultMethod = defaultMethod;
|
|
1231
|
+
}
|
|
1232
|
+
if (metadataOverrides) {
|
|
1233
|
+
config.metadataOverrides = metadataOverrides;
|
|
1234
|
+
}
|
|
1235
|
+
return config;
|
|
1236
|
+
}
|
|
1237
|
+
function isPlainObject(value) {
|
|
1238
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1239
|
+
}
|
|
1240
|
+
function findDuplicates(values) {
|
|
1241
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1242
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
1243
|
+
values.forEach((value) => {
|
|
1244
|
+
const count = seen.get(value) ?? 0;
|
|
1245
|
+
seen.set(value, count + 1);
|
|
1246
|
+
if (count >= 1) {
|
|
1247
|
+
duplicates.add(value);
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
return Array.from(duplicates.values());
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
export { loadAndValidateTools };
|
|
1254
|
+
//# sourceMappingURL=index.js.map
|
|
1255
|
+
//# sourceMappingURL=index.js.map
|