opentool 0.4.6 → 0.6.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 +95 -30
- package/dist/ai/index.d.ts +237 -0
- package/dist/ai/index.js +759 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/cli/index.d.ts +38 -5
- package/dist/cli/index.js +2218 -67
- package/dist/cli/index.js.map +1 -1
- package/dist/index-D3DaM5Rs.d.ts +1693 -0
- package/dist/index.d.ts +33 -5
- package/dist/index.js +3258 -25
- package/dist/index.js.map +1 -1
- package/dist/payment/index.d.ts +2 -0
- package/dist/payment/index.js +969 -0
- package/dist/payment/index.js.map +1 -0
- package/dist/validate-DiIOFUU5.d.ts +828 -0
- package/dist/wallets/index.d.ts +117 -0
- package/dist/wallets/index.js +337 -0
- package/dist/wallets/index.js.map +1 -0
- package/package.json +39 -4
- package/dist/cli/build.d.ts +0 -8
- package/dist/cli/build.d.ts.map +0 -1
- package/dist/cli/build.js +0 -508
- package/dist/cli/build.js.map +0 -1
- package/dist/cli/dev.d.ts +0 -6
- package/dist/cli/dev.d.ts.map +0 -1
- package/dist/cli/dev.js +0 -168
- package/dist/cli/dev.js.map +0 -1
- package/dist/cli/generate-metadata.d.ts +0 -15
- package/dist/cli/generate-metadata.d.ts.map +0 -1
- package/dist/cli/generate-metadata.js +0 -291
- package/dist/cli/generate-metadata.js.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/validate.d.ts +0 -8
- package/dist/cli/validate.d.ts.map +0 -1
- package/dist/cli/validate.js +0 -328
- package/dist/cli/validate.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/runtime/index.d.ts +0 -11
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/index.js +0 -192
- package/dist/runtime/index.js.map +0 -1
- package/dist/types/index.d.ts +0 -50
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +0 -1
- package/dist/types/metadata.d.ts +0 -65
- package/dist/types/metadata.d.ts.map +0 -1
- package/dist/types/metadata.js +0 -3
- package/dist/types/metadata.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,70 +1,2221 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import * as fs4 from 'fs';
|
|
4
|
+
import * as path5 from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { build } from 'esbuild';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
10
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
11
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
12
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
+
import * as http from 'http';
|
|
15
|
+
|
|
16
|
+
function resolveTsconfig(projectRoot) {
|
|
17
|
+
const candidate = path5.join(projectRoot, "tsconfig.json");
|
|
18
|
+
if (fs4.existsSync(candidate)) {
|
|
19
|
+
return candidate;
|
|
20
|
+
}
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
23
|
+
async function transpileWithEsbuild(options) {
|
|
24
|
+
if (options.entryPoints.length === 0) {
|
|
25
|
+
throw new Error("No entry points provided for esbuild transpilation");
|
|
26
|
+
}
|
|
27
|
+
const projectRoot = options.projectRoot;
|
|
28
|
+
const tempBase = options.outDir ?? fs4.mkdtempSync(path5.join(tmpdir(), "opentool-"));
|
|
29
|
+
if (!fs4.existsSync(tempBase)) {
|
|
30
|
+
fs4.mkdirSync(tempBase, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
const tsconfig = resolveTsconfig(projectRoot);
|
|
33
|
+
const buildOptions = {
|
|
34
|
+
entryPoints: options.entryPoints,
|
|
35
|
+
outdir: tempBase,
|
|
36
|
+
bundle: false,
|
|
37
|
+
format: options.format,
|
|
38
|
+
platform: "node",
|
|
39
|
+
target: "node20",
|
|
40
|
+
logLevel: "warning",
|
|
41
|
+
sourcesContent: false,
|
|
42
|
+
sourcemap: false,
|
|
43
|
+
packages: "external",
|
|
44
|
+
loader: {
|
|
45
|
+
".ts": "ts",
|
|
46
|
+
".tsx": "tsx",
|
|
47
|
+
".cts": "ts",
|
|
48
|
+
".mts": "ts",
|
|
49
|
+
".js": "js",
|
|
50
|
+
".jsx": "jsx",
|
|
51
|
+
".mjs": "js",
|
|
52
|
+
".cjs": "js"
|
|
53
|
+
},
|
|
54
|
+
metafile: false,
|
|
55
|
+
allowOverwrite: true,
|
|
56
|
+
absWorkingDir: projectRoot
|
|
57
|
+
};
|
|
58
|
+
if (tsconfig) {
|
|
59
|
+
buildOptions.tsconfig = tsconfig;
|
|
60
|
+
}
|
|
61
|
+
await build(buildOptions);
|
|
62
|
+
if (options.format === "esm") {
|
|
63
|
+
const packageJsonPath = path5.join(tempBase, "package.json");
|
|
64
|
+
if (!fs4.existsSync(packageJsonPath)) {
|
|
65
|
+
fs4.writeFileSync(packageJsonPath, JSON.stringify({ type: "module" }), "utf8");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const cleanup = () => {
|
|
69
|
+
if (options.outDir) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
fs4.rmSync(tempBase, { recursive: true, force: true });
|
|
73
|
+
};
|
|
74
|
+
return { outDir: tempBase, cleanup };
|
|
75
|
+
}
|
|
76
|
+
var METADATA_SPEC_VERSION = "1.0.0";
|
|
77
|
+
var McpAnnotationsSchema = z.object({
|
|
78
|
+
title: z.string().optional(),
|
|
79
|
+
readOnlyHint: z.boolean().optional(),
|
|
80
|
+
destructiveHint: z.boolean().optional(),
|
|
81
|
+
idempotentHint: z.boolean().optional(),
|
|
82
|
+
openWorldHint: z.boolean().optional(),
|
|
83
|
+
requiresPayment: z.boolean().optional()
|
|
84
|
+
}).strict();
|
|
85
|
+
var PaymentConfigSchema = z.object({
|
|
86
|
+
amountUSDC: z.number().nonnegative().optional(),
|
|
87
|
+
description: z.string().optional(),
|
|
88
|
+
x402: z.boolean().optional(),
|
|
89
|
+
plain402: z.boolean().optional(),
|
|
90
|
+
acceptedMethods: z.array(z.union([z.literal("x402"), z.literal("402")])).optional(),
|
|
91
|
+
acceptedCurrencies: z.array(z.string()).optional(),
|
|
92
|
+
chainIds: z.array(z.number().int()).optional(),
|
|
93
|
+
facilitator: z.string().optional()
|
|
94
|
+
}).strict();
|
|
95
|
+
var DiscoveryMetadataSchema = z.object({
|
|
96
|
+
keywords: z.array(z.string()).optional(),
|
|
97
|
+
category: z.string().optional(),
|
|
98
|
+
useCases: z.array(z.string()).optional(),
|
|
99
|
+
capabilities: z.array(z.string()).optional(),
|
|
100
|
+
requirements: z.record(z.any()).optional(),
|
|
101
|
+
pricing: z.record(z.any()).optional(),
|
|
102
|
+
compatibility: z.record(z.any()).optional(),
|
|
103
|
+
documentation: z.union([z.string(), z.array(z.string())]).optional()
|
|
104
|
+
}).catchall(z.any());
|
|
105
|
+
var ToolMetadataOverridesSchema = z.object({
|
|
106
|
+
name: z.string().optional(),
|
|
107
|
+
description: z.string().optional(),
|
|
108
|
+
annotations: McpAnnotationsSchema.optional(),
|
|
109
|
+
payment: PaymentConfigSchema.optional(),
|
|
110
|
+
discovery: DiscoveryMetadataSchema.optional()
|
|
111
|
+
}).catchall(z.any());
|
|
112
|
+
var ToolSchema = z.object({
|
|
113
|
+
name: z.string(),
|
|
114
|
+
description: z.string(),
|
|
115
|
+
inputSchema: z.any(),
|
|
116
|
+
annotations: McpAnnotationsSchema.optional(),
|
|
117
|
+
payment: PaymentConfigSchema.optional(),
|
|
118
|
+
discovery: DiscoveryMetadataSchema.optional()
|
|
119
|
+
}).strict();
|
|
120
|
+
var AuthoredMetadataSchema = z.object({
|
|
121
|
+
metadataSpecVersion: z.string().optional(),
|
|
122
|
+
name: z.string().optional(),
|
|
123
|
+
displayName: z.string().optional(),
|
|
124
|
+
version: z.string().optional(),
|
|
125
|
+
description: z.string().optional(),
|
|
126
|
+
author: z.string().optional(),
|
|
127
|
+
repository: z.string().optional(),
|
|
128
|
+
website: z.string().optional(),
|
|
129
|
+
category: z.string().optional(),
|
|
130
|
+
categories: z.array(z.string()).optional(),
|
|
131
|
+
termsOfService: z.string().optional(),
|
|
132
|
+
mcpUrl: z.string().optional(),
|
|
133
|
+
payment: PaymentConfigSchema.optional(),
|
|
134
|
+
discovery: DiscoveryMetadataSchema.optional(),
|
|
135
|
+
promptExamples: z.array(z.string()).optional(),
|
|
136
|
+
iconPath: z.string().optional(),
|
|
137
|
+
videoPath: z.string().optional(),
|
|
138
|
+
image: z.string().optional(),
|
|
139
|
+
animation_url: z.string().optional(),
|
|
140
|
+
keywords: z.array(z.string()).optional(),
|
|
141
|
+
useCases: z.array(z.string()).optional(),
|
|
142
|
+
capabilities: z.array(z.string()).optional(),
|
|
143
|
+
requirements: z.record(z.any()).optional(),
|
|
144
|
+
pricing: z.record(z.any()).optional(),
|
|
145
|
+
compatibility: z.record(z.any()).optional()
|
|
146
|
+
}).catchall(z.any());
|
|
147
|
+
var MetadataSchema = z.object({
|
|
148
|
+
metadataSpecVersion: z.string().default(METADATA_SPEC_VERSION),
|
|
149
|
+
name: z.string(),
|
|
150
|
+
displayName: z.string(),
|
|
151
|
+
version: z.string(),
|
|
152
|
+
description: z.string().optional(),
|
|
153
|
+
author: z.string().optional(),
|
|
154
|
+
repository: z.string().optional(),
|
|
155
|
+
website: z.string().optional(),
|
|
156
|
+
category: z.string(),
|
|
157
|
+
termsOfService: z.string().optional(),
|
|
158
|
+
mcpUrl: z.string().optional(),
|
|
159
|
+
payment: PaymentConfigSchema.optional(),
|
|
160
|
+
tools: z.array(ToolSchema).min(1),
|
|
161
|
+
discovery: DiscoveryMetadataSchema.optional(),
|
|
162
|
+
promptExamples: z.array(z.string()).optional(),
|
|
163
|
+
iconPath: z.string().optional(),
|
|
164
|
+
videoPath: z.string().optional(),
|
|
165
|
+
image: z.string().optional(),
|
|
166
|
+
animation_url: z.string().optional()
|
|
167
|
+
}).strict();
|
|
168
|
+
createRequire(import.meta.url);
|
|
169
|
+
function resolveCompiledPath(outDir, originalFile, extension = ".js") {
|
|
170
|
+
const baseName = path5.basename(originalFile).replace(/\.[^.]+$/, "");
|
|
171
|
+
return path5.join(outDir, `${baseName}${extension}`);
|
|
172
|
+
}
|
|
173
|
+
async function importFresh(modulePath) {
|
|
174
|
+
const fileUrl = pathToFileURL(modulePath).href;
|
|
175
|
+
const cacheBuster = `t=${Date.now()}-${Math.random()}`;
|
|
176
|
+
const separator = fileUrl.includes("?") ? "&" : "?";
|
|
177
|
+
return import(`${fileUrl}${separator}${cacheBuster}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/cli/shared/metadata.ts
|
|
181
|
+
var METADATA_ENTRY = "metadata.ts";
|
|
182
|
+
async function loadAuthoredMetadata(projectRoot) {
|
|
183
|
+
const absPath = path5.join(projectRoot, METADATA_ENTRY);
|
|
184
|
+
if (!fs4.existsSync(absPath)) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`metadata.ts not found in ${projectRoot}. Create metadata.ts to describe your agent.`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const tempDir = path5.join(projectRoot, ".opentool-temp");
|
|
190
|
+
if (fs4.existsSync(tempDir)) {
|
|
191
|
+
fs4.rmSync(tempDir, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
const { outDir, cleanup } = await transpileWithEsbuild({
|
|
194
|
+
entryPoints: [absPath],
|
|
195
|
+
projectRoot,
|
|
196
|
+
format: "esm",
|
|
197
|
+
outDir: tempDir
|
|
198
|
+
});
|
|
199
|
+
try {
|
|
200
|
+
const compiledPath = resolveCompiledPath(outDir, METADATA_ENTRY);
|
|
201
|
+
const moduleExports = await importFresh(compiledPath);
|
|
202
|
+
const metadataExport = extractMetadataExport(moduleExports);
|
|
203
|
+
const parsed = AuthoredMetadataSchema.parse(metadataExport);
|
|
204
|
+
return { metadata: parsed, sourcePath: absPath };
|
|
205
|
+
} finally {
|
|
206
|
+
cleanup();
|
|
207
|
+
if (fs4.existsSync(tempDir)) {
|
|
208
|
+
fs4.rmSync(tempDir, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function extractMetadataExport(moduleExports) {
|
|
213
|
+
if (!moduleExports || typeof moduleExports !== "object") {
|
|
214
|
+
throw new Error("metadata.ts must export a metadata object");
|
|
215
|
+
}
|
|
216
|
+
const exportsObject = moduleExports;
|
|
217
|
+
if (exportsObject.metadata) {
|
|
218
|
+
return exportsObject.metadata;
|
|
219
|
+
}
|
|
220
|
+
if (exportsObject.default && typeof exportsObject.default === "object") {
|
|
221
|
+
const defaultExport = exportsObject.default;
|
|
222
|
+
if (defaultExport.metadata) {
|
|
223
|
+
return defaultExport.metadata;
|
|
224
|
+
}
|
|
225
|
+
return defaultExport;
|
|
226
|
+
}
|
|
227
|
+
return moduleExports;
|
|
228
|
+
}
|
|
229
|
+
function readPackageJson(projectRoot) {
|
|
230
|
+
const packagePath = path5.join(projectRoot, "package.json");
|
|
231
|
+
if (!fs4.existsSync(packagePath)) {
|
|
232
|
+
return {};
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const content = fs4.readFileSync(packagePath, "utf8");
|
|
236
|
+
return JSON.parse(content);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
throw new Error(`Failed to read package.json: ${error}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function buildMetadataArtifact(options) {
|
|
242
|
+
const projectRoot = options.projectRoot;
|
|
243
|
+
const packageInfo = readPackageJson(projectRoot);
|
|
244
|
+
const { metadata: authored, sourcePath } = await loadAuthoredMetadata(projectRoot);
|
|
245
|
+
const defaultsApplied = [];
|
|
246
|
+
const folderName = path5.basename(projectRoot);
|
|
247
|
+
const name = resolveField(
|
|
248
|
+
"name",
|
|
249
|
+
authored.name,
|
|
250
|
+
() => packageInfo.name ?? folderName,
|
|
251
|
+
defaultsApplied,
|
|
252
|
+
"package.json name"
|
|
253
|
+
);
|
|
254
|
+
const displayName = resolveField(
|
|
255
|
+
"displayName",
|
|
256
|
+
authored.displayName,
|
|
257
|
+
() => {
|
|
258
|
+
const source = packageInfo.name ?? folderName;
|
|
259
|
+
return source.split(/[-_]/).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
|
|
260
|
+
},
|
|
261
|
+
defaultsApplied,
|
|
262
|
+
"package.json name"
|
|
263
|
+
);
|
|
264
|
+
const versionRaw = resolveField(
|
|
265
|
+
"version",
|
|
266
|
+
authored.version,
|
|
267
|
+
() => packageInfo.version ?? "0.1.0",
|
|
268
|
+
defaultsApplied,
|
|
269
|
+
"package.json version"
|
|
270
|
+
);
|
|
271
|
+
const version = typeof versionRaw === "number" ? String(versionRaw) : versionRaw;
|
|
272
|
+
const category = determineCategory(authored, defaultsApplied);
|
|
273
|
+
const description = authored.description ?? packageInfo.description;
|
|
274
|
+
if (!authored.description && packageInfo.description) {
|
|
275
|
+
defaultsApplied.push("description \u2192 package.json description");
|
|
276
|
+
}
|
|
277
|
+
const author = authored.author ?? packageInfo.author;
|
|
278
|
+
if (!authored.author && packageInfo.author) {
|
|
279
|
+
defaultsApplied.push("author \u2192 package.json author");
|
|
280
|
+
}
|
|
281
|
+
const repository = authored.repository ?? extractRepository(packageInfo.repository);
|
|
282
|
+
if (!authored.repository && repository) {
|
|
283
|
+
defaultsApplied.push("repository \u2192 package.json repository");
|
|
284
|
+
}
|
|
285
|
+
const website = authored.website ?? packageInfo.homepage;
|
|
286
|
+
if (!authored.website && packageInfo.homepage) {
|
|
287
|
+
defaultsApplied.push("website \u2192 package.json homepage");
|
|
288
|
+
}
|
|
289
|
+
const payment = resolvePayment(authored, defaultsApplied);
|
|
290
|
+
const baseImage = authored.image ?? authored.iconPath;
|
|
291
|
+
const animation = authored.animation_url ?? authored.videoPath;
|
|
292
|
+
const discovery = buildDiscovery(authored);
|
|
293
|
+
const metadataTools = options.tools.map((tool) => {
|
|
294
|
+
const overrides = tool.metadata ? ToolMetadataOverridesSchema.parse(tool.metadata) : {};
|
|
295
|
+
const toolName = overrides.name ?? tool.filename;
|
|
296
|
+
const toolDescription = overrides.description ?? `${toolName} tool`;
|
|
297
|
+
const toolPayment = overrides.payment ?? payment ?? void 0;
|
|
298
|
+
if (!overrides.payment && toolPayment && payment && toolPayment === payment) {
|
|
299
|
+
defaultsApplied.push(`tool ${toolName} payment \u2192 agent payment`);
|
|
300
|
+
}
|
|
301
|
+
const toolDiscovery = overrides.discovery ?? void 0;
|
|
302
|
+
const toolDefinition = {
|
|
303
|
+
name: toolName,
|
|
304
|
+
description: toolDescription,
|
|
305
|
+
inputSchema: tool.inputSchema
|
|
306
|
+
};
|
|
307
|
+
if (overrides.annotations) {
|
|
308
|
+
toolDefinition.annotations = overrides.annotations;
|
|
309
|
+
}
|
|
310
|
+
if (toolPayment) {
|
|
311
|
+
toolDefinition.payment = toolPayment;
|
|
312
|
+
}
|
|
313
|
+
if (toolDiscovery) {
|
|
314
|
+
toolDefinition.discovery = toolDiscovery;
|
|
315
|
+
}
|
|
316
|
+
return toolDefinition;
|
|
317
|
+
});
|
|
318
|
+
const metadata = MetadataSchema.parse({
|
|
319
|
+
metadataSpecVersion: authored.metadataSpecVersion ?? METADATA_SPEC_VERSION,
|
|
320
|
+
name,
|
|
321
|
+
displayName,
|
|
322
|
+
version,
|
|
323
|
+
description,
|
|
324
|
+
author,
|
|
325
|
+
repository,
|
|
326
|
+
website,
|
|
327
|
+
category,
|
|
328
|
+
termsOfService: authored.termsOfService,
|
|
329
|
+
mcpUrl: authored.mcpUrl,
|
|
330
|
+
payment: payment ?? void 0,
|
|
331
|
+
tools: metadataTools,
|
|
332
|
+
discovery,
|
|
333
|
+
promptExamples: authored.promptExamples,
|
|
334
|
+
iconPath: authored.iconPath,
|
|
335
|
+
videoPath: authored.videoPath,
|
|
336
|
+
image: baseImage,
|
|
337
|
+
animation_url: animation
|
|
338
|
+
});
|
|
339
|
+
return {
|
|
340
|
+
metadata,
|
|
341
|
+
defaultsApplied,
|
|
342
|
+
sourceMetadataPath: sourcePath
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function resolveField(field, value, fallback, defaultsApplied, fallbackLabel) {
|
|
346
|
+
if (value !== void 0 && value !== null && value !== "") {
|
|
347
|
+
return value;
|
|
348
|
+
}
|
|
349
|
+
const resolved = fallback();
|
|
350
|
+
defaultsApplied.push(`${field} \u2192 ${fallbackLabel}`);
|
|
351
|
+
return resolved;
|
|
352
|
+
}
|
|
353
|
+
function determineCategory(authored, defaultsApplied) {
|
|
354
|
+
if (authored.category) {
|
|
355
|
+
return authored.category;
|
|
356
|
+
}
|
|
357
|
+
if (Array.isArray(authored.categories) && authored.categories.length > 0) {
|
|
358
|
+
defaultsApplied.push("category \u2192 metadata.categories[0]");
|
|
359
|
+
return authored.categories[0];
|
|
360
|
+
}
|
|
361
|
+
defaultsApplied.push("category \u2192 default category");
|
|
362
|
+
return "utility";
|
|
363
|
+
}
|
|
364
|
+
function extractRepository(repository) {
|
|
365
|
+
if (!repository) {
|
|
366
|
+
return void 0;
|
|
367
|
+
}
|
|
368
|
+
if (typeof repository === "string") {
|
|
369
|
+
return repository;
|
|
370
|
+
}
|
|
371
|
+
return repository.url;
|
|
372
|
+
}
|
|
373
|
+
function resolvePayment(authored, defaults) {
|
|
374
|
+
if (authored.payment) {
|
|
375
|
+
return authored.payment;
|
|
376
|
+
}
|
|
377
|
+
const discoveryPricing = authored.discovery?.pricing;
|
|
378
|
+
const legacyPricing = authored.pricing;
|
|
379
|
+
const pricing = discoveryPricing ?? legacyPricing;
|
|
380
|
+
if (!pricing) {
|
|
381
|
+
return void 0;
|
|
382
|
+
}
|
|
383
|
+
const amount = typeof pricing.defaultAmount === "number" ? pricing.defaultAmount : 0;
|
|
384
|
+
const sourceLabel = discoveryPricing ? "discovery.pricing.defaultAmount" : "pricing.defaultAmount";
|
|
385
|
+
defaults.push(`payment \u2192 synthesized from ${sourceLabel}`);
|
|
386
|
+
const acceptedMethodsRaw = Array.isArray(pricing.acceptedMethods) ? pricing.acceptedMethods : ["402"];
|
|
387
|
+
const acceptedMethods = acceptedMethodsRaw.map(
|
|
388
|
+
(method) => method === "x402" ? "x402" : "402"
|
|
389
|
+
);
|
|
390
|
+
return {
|
|
391
|
+
amountUSDC: amount,
|
|
392
|
+
description: typeof pricing.description === "string" ? pricing.description : void 0,
|
|
393
|
+
x402: acceptedMethods.includes("x402"),
|
|
394
|
+
plain402: acceptedMethods.includes("402"),
|
|
395
|
+
acceptedMethods,
|
|
396
|
+
acceptedCurrencies: Array.isArray(pricing.acceptedCurrencies) ? pricing.acceptedCurrencies : ["USDC"],
|
|
397
|
+
chainIds: Array.isArray(pricing.chainIds) ? pricing.chainIds : [8453]
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function buildDiscovery(authored) {
|
|
401
|
+
const legacyDiscovery = {};
|
|
402
|
+
if (Array.isArray(authored.keywords) && authored.keywords.length > 0) {
|
|
403
|
+
legacyDiscovery.keywords = authored.keywords;
|
|
404
|
+
}
|
|
405
|
+
if (Array.isArray(authored.useCases) && authored.useCases.length > 0) {
|
|
406
|
+
legacyDiscovery.useCases = authored.useCases;
|
|
407
|
+
}
|
|
408
|
+
if (Array.isArray(authored.capabilities) && authored.capabilities.length > 0) {
|
|
409
|
+
legacyDiscovery.capabilities = authored.capabilities;
|
|
410
|
+
}
|
|
411
|
+
if (authored.requirements) {
|
|
412
|
+
legacyDiscovery.requirements = authored.requirements;
|
|
413
|
+
}
|
|
414
|
+
if (authored.compatibility) {
|
|
415
|
+
legacyDiscovery.compatibility = authored.compatibility;
|
|
416
|
+
}
|
|
417
|
+
if (Array.isArray(authored.categories) && authored.categories.length > 0) {
|
|
418
|
+
legacyDiscovery.category = authored.categories[0];
|
|
419
|
+
}
|
|
420
|
+
if (authored.pricing) {
|
|
421
|
+
legacyDiscovery.pricing = authored.pricing;
|
|
422
|
+
}
|
|
423
|
+
const merged = {
|
|
424
|
+
...legacyDiscovery,
|
|
425
|
+
...authored.discovery ?? {}
|
|
426
|
+
};
|
|
427
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
428
|
+
}
|
|
429
|
+
var PAYMENT_SCHEMA_VERSION = 1;
|
|
430
|
+
var paymentSchemaVersionSchema = z.literal(PAYMENT_SCHEMA_VERSION);
|
|
431
|
+
var decimalStringSchema = z.string().regex(/^(?:0|[1-9]\d*)(?:\.\d+)?$/, "Value must be a positive decimal string");
|
|
432
|
+
var currencySchema = z.object({
|
|
433
|
+
code: z.string().min(2).max(12).transform((value) => value.toUpperCase()),
|
|
434
|
+
symbol: z.string().min(1).max(6).optional(),
|
|
435
|
+
decimals: z.number().int().min(0).max(36).optional(),
|
|
436
|
+
kind: z.enum(["fiat", "crypto"]).default("crypto").optional(),
|
|
437
|
+
description: z.string().optional()
|
|
438
|
+
});
|
|
439
|
+
var paymentAmountSchema = z.object({
|
|
440
|
+
value: decimalStringSchema,
|
|
441
|
+
currency: currencySchema,
|
|
442
|
+
display: z.string().optional()
|
|
443
|
+
});
|
|
444
|
+
var cryptoAssetSchema = z.object({
|
|
445
|
+
symbol: z.string().min(2).max(12),
|
|
446
|
+
network: z.string().min(1).optional(),
|
|
447
|
+
chainId: z.number().int().min(0).optional(),
|
|
448
|
+
address: z.string().min(1).optional(),
|
|
449
|
+
decimals: z.number().int().min(0).max(36).optional(),
|
|
450
|
+
standard: z.enum(["erc20", "spl", "custom"]).default("erc20").optional(),
|
|
451
|
+
description: z.string().optional()
|
|
452
|
+
});
|
|
453
|
+
var facilitatorConfigSchema = z.object({
|
|
454
|
+
url: z.string().url(),
|
|
455
|
+
vendor: z.string().optional(),
|
|
456
|
+
verifyPath: z.string().default("/verify").optional(),
|
|
457
|
+
settlePath: z.string().default("/settle").optional(),
|
|
458
|
+
apiKey: z.string().optional(),
|
|
459
|
+
apiKeyEnv: z.string().optional(),
|
|
460
|
+
apiKeyHeader: z.string().default("Authorization").optional(),
|
|
461
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
462
|
+
timeoutMs: z.number().int().positive().optional()
|
|
463
|
+
});
|
|
464
|
+
var settlementTermsSchema = z.object({
|
|
465
|
+
windowSeconds: z.number().int().positive().optional(),
|
|
466
|
+
targetConfirmations: z.number().int().positive().optional(),
|
|
467
|
+
finalityDescription: z.string().optional(),
|
|
468
|
+
slaDescription: z.string().optional()
|
|
469
|
+
});
|
|
470
|
+
var paymentFieldSchema = z.object({
|
|
471
|
+
key: z.string().min(1),
|
|
472
|
+
label: z.string().min(1),
|
|
473
|
+
required: z.boolean().default(true).optional(),
|
|
474
|
+
description: z.string().optional(),
|
|
475
|
+
example: z.string().optional()
|
|
476
|
+
});
|
|
477
|
+
var x402ProofSchema = z.object({
|
|
478
|
+
mode: z.literal("x402"),
|
|
479
|
+
scheme: z.string().min(1),
|
|
480
|
+
network: z.string().min(1),
|
|
481
|
+
version: z.number().int().min(1).optional(),
|
|
482
|
+
facilitator: facilitatorConfigSchema.optional(),
|
|
483
|
+
verifier: z.string().optional()
|
|
484
|
+
});
|
|
485
|
+
var directProofSchema = z.object({
|
|
486
|
+
mode: z.literal("direct"),
|
|
487
|
+
proofTypes: z.array(z.string().min(1)).nonempty(),
|
|
488
|
+
verifier: z.string().optional(),
|
|
489
|
+
instructions: z.string().optional(),
|
|
490
|
+
fields: z.array(paymentFieldSchema).optional(),
|
|
491
|
+
allowsManualReview: z.boolean().optional()
|
|
492
|
+
});
|
|
493
|
+
var paymentProofSchema = z.discriminatedUnion("mode", [
|
|
494
|
+
x402ProofSchema,
|
|
495
|
+
directProofSchema
|
|
496
|
+
]);
|
|
497
|
+
var paymentOptionSchema = z.object({
|
|
498
|
+
id: z.string().min(1),
|
|
499
|
+
title: z.string().min(1),
|
|
500
|
+
description: z.string().optional(),
|
|
501
|
+
amount: paymentAmountSchema,
|
|
502
|
+
asset: cryptoAssetSchema,
|
|
503
|
+
payTo: z.string().min(1),
|
|
504
|
+
resource: z.string().url().optional(),
|
|
505
|
+
proof: paymentProofSchema,
|
|
506
|
+
settlement: settlementTermsSchema.optional(),
|
|
507
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
508
|
+
});
|
|
509
|
+
var paymentRequirementsSchema = z.object({
|
|
510
|
+
schemaVersion: paymentSchemaVersionSchema,
|
|
511
|
+
message: z.string().optional(),
|
|
512
|
+
title: z.string().optional(),
|
|
513
|
+
resource: z.string().url().optional(),
|
|
514
|
+
accepts: z.array(paymentOptionSchema).nonempty(),
|
|
515
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
516
|
+
fallbackText: z.string().optional()
|
|
517
|
+
});
|
|
518
|
+
var x402PaymentHeaderSchema = z.object({
|
|
519
|
+
x402Version: z.number().int().min(1),
|
|
520
|
+
scheme: z.string().min(1),
|
|
521
|
+
network: z.string().min(1),
|
|
522
|
+
payload: z.unknown(),
|
|
523
|
+
correlationId: z.string().optional()
|
|
524
|
+
});
|
|
525
|
+
var directPaymentPayloadSchema = z.object({
|
|
526
|
+
schemaVersion: z.literal(1),
|
|
527
|
+
optionId: z.string().min(1),
|
|
528
|
+
proofType: z.string().min(1),
|
|
529
|
+
payload: z.unknown(),
|
|
530
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
531
|
+
});
|
|
532
|
+
var paymentSuccessMetadataSchema = z.object({
|
|
533
|
+
optionId: z.string().min(1),
|
|
534
|
+
verifier: z.string().optional(),
|
|
535
|
+
txHash: z.string().optional(),
|
|
536
|
+
networkId: z.string().optional(),
|
|
537
|
+
amount: paymentAmountSchema.optional(),
|
|
538
|
+
settledAt: z.string().datetime().optional(),
|
|
539
|
+
payload: z.unknown().optional()
|
|
540
|
+
});
|
|
541
|
+
var paymentFailureSchema = z.object({
|
|
542
|
+
reason: z.string().min(1),
|
|
543
|
+
code: z.enum([
|
|
544
|
+
"verifier_not_found",
|
|
545
|
+
"verification_failed",
|
|
546
|
+
"invalid_payload",
|
|
547
|
+
"unsupported_option",
|
|
548
|
+
"missing_header",
|
|
549
|
+
"unknown"
|
|
550
|
+
]).default("unknown").optional(),
|
|
551
|
+
retryable: z.boolean().optional(),
|
|
552
|
+
detail: z.unknown().optional()
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// src/helpers/payment.ts
|
|
556
|
+
var X402_VERSION_DEFAULT = 1;
|
|
557
|
+
var HEADER_X402 = "X-PAYMENT";
|
|
558
|
+
var HEADER_DIRECT = "X-PAYMENT-PROOF";
|
|
559
|
+
var HEADER_PAYMENT_RESPONSE = "X-PAYMENT-RESPONSE";
|
|
560
|
+
var x402RequirementSchema = z.object({
|
|
561
|
+
scheme: z.string().min(1),
|
|
562
|
+
network: z.string().min(1),
|
|
563
|
+
maxAmountRequired: z.string().min(1),
|
|
564
|
+
asset: z.string().min(1),
|
|
565
|
+
payTo: z.string().min(1),
|
|
566
|
+
resource: z.string().optional(),
|
|
567
|
+
description: z.string().optional(),
|
|
568
|
+
mimeType: z.string().optional(),
|
|
569
|
+
outputSchema: z.unknown().optional(),
|
|
570
|
+
maxTimeoutSeconds: z.number().int().positive().optional(),
|
|
571
|
+
extra: z.record(z.string(), z.unknown()).nullable().optional()
|
|
572
|
+
});
|
|
573
|
+
function createPaymentRequiredBody(definition) {
|
|
574
|
+
const parsed = paymentRequirementsSchema.parse(definition);
|
|
575
|
+
const x402Accepts = parsed.accepts.filter((option) => option.proof.mode === "x402").map(
|
|
576
|
+
(option) => toX402Requirement(option, parsed.resource, option.settlement)
|
|
577
|
+
).filter((value) => Boolean(value));
|
|
578
|
+
const x402Body = x402Accepts.length > 0 ? {
|
|
579
|
+
x402Version: resolveX402Version(parsed.accepts),
|
|
580
|
+
error: parsed.message ?? "Payment required",
|
|
581
|
+
accepts: x402Accepts
|
|
582
|
+
} : void 0;
|
|
583
|
+
if (x402Body) {
|
|
584
|
+
return {
|
|
585
|
+
...parsed,
|
|
586
|
+
x402: x402Body
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
return parsed;
|
|
590
|
+
}
|
|
591
|
+
function paymentRequiredResponse(definition, init) {
|
|
592
|
+
const body = createPaymentRequiredBody(definition);
|
|
593
|
+
const headers = new Headers(init?.headers);
|
|
594
|
+
if (!headers.has("content-type")) {
|
|
595
|
+
headers.set("content-type", "application/json; charset=utf-8");
|
|
596
|
+
}
|
|
597
|
+
return new Response(JSON.stringify(body), {
|
|
598
|
+
...init,
|
|
599
|
+
status: 402,
|
|
600
|
+
headers
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
function extractPaymentAttempts(source) {
|
|
604
|
+
const attempts = [];
|
|
605
|
+
const failures = [];
|
|
606
|
+
const x402Header = source.headers.get(HEADER_X402);
|
|
607
|
+
if (x402Header) {
|
|
608
|
+
const { attempt, failure } = parseX402Header(x402Header);
|
|
609
|
+
if (attempt) {
|
|
610
|
+
attempts.push(attempt);
|
|
611
|
+
} else if (failure) {
|
|
612
|
+
failures.push(failure);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const directHeader = source.headers.get(HEADER_DIRECT);
|
|
616
|
+
if (directHeader) {
|
|
617
|
+
const { attempt, failure } = parseDirectHeader(directHeader);
|
|
618
|
+
if (attempt) {
|
|
619
|
+
attempts.push(attempt);
|
|
620
|
+
} else if (failure) {
|
|
621
|
+
failures.push(failure);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (attempts.length === 0 && failures.length === 0) {
|
|
625
|
+
failures.push(
|
|
626
|
+
paymentFailureSchema.parse({
|
|
627
|
+
reason: "No payment headers present",
|
|
628
|
+
code: "missing_header",
|
|
629
|
+
retryable: false
|
|
630
|
+
})
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
return { attempts, failures };
|
|
634
|
+
}
|
|
635
|
+
async function verifyPayment(options) {
|
|
636
|
+
const definition = paymentRequirementsSchema.parse(options.definition);
|
|
637
|
+
const attempts = options.attempts ? options.attempts : options.request ? extractPaymentAttempts(options.request).attempts : [];
|
|
638
|
+
if (attempts.length === 0) {
|
|
639
|
+
return {
|
|
640
|
+
success: false,
|
|
641
|
+
optionId: "",
|
|
642
|
+
attemptType: "direct",
|
|
643
|
+
failure: paymentFailureSchema.parse({
|
|
644
|
+
reason: "No payment attempt found",
|
|
645
|
+
code: "missing_header",
|
|
646
|
+
retryable: false
|
|
647
|
+
})
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
for (const attempt of attempts) {
|
|
651
|
+
const option = findMatchingOption(definition, attempt);
|
|
652
|
+
if (!option) {
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (attempt.type === "x402") {
|
|
656
|
+
const proof = option.proof;
|
|
657
|
+
const verifierId = proof.verifier ?? (proof.facilitator ? "x402:facilitator" : void 0);
|
|
658
|
+
if (verifierId === "x402:facilitator" && proof.facilitator) {
|
|
659
|
+
const context2 = {
|
|
660
|
+
attempt,
|
|
661
|
+
option,
|
|
662
|
+
definition
|
|
663
|
+
};
|
|
664
|
+
if (options.settle !== void 0) {
|
|
665
|
+
context2.settle = options.settle;
|
|
666
|
+
}
|
|
667
|
+
return runFacilitatorVerifier({
|
|
668
|
+
...context2,
|
|
669
|
+
fetchImpl: options.fetchImpl ?? fetch
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
const verifier = verifierId ? options.verifiers?.[verifierId] : void 0;
|
|
673
|
+
if (!verifier) {
|
|
674
|
+
return {
|
|
675
|
+
success: false,
|
|
676
|
+
optionId: option.id,
|
|
677
|
+
attemptType: attempt.type,
|
|
678
|
+
failure: paymentFailureSchema.parse({
|
|
679
|
+
reason: `No verifier registered for id: ${verifierId ?? "(missing)"}`,
|
|
680
|
+
code: "verifier_not_found",
|
|
681
|
+
retryable: false
|
|
682
|
+
})
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
const context = {
|
|
686
|
+
attempt,
|
|
687
|
+
option,
|
|
688
|
+
definition
|
|
689
|
+
};
|
|
690
|
+
if (options.settle !== void 0) {
|
|
691
|
+
context.settle = options.settle;
|
|
692
|
+
}
|
|
693
|
+
return verifier(context);
|
|
694
|
+
}
|
|
695
|
+
if (attempt.type === "direct") {
|
|
696
|
+
const proof = option.proof;
|
|
697
|
+
const verifierId = proof.verifier ?? `direct:${attempt.payload.proofType}`;
|
|
698
|
+
const verifier = verifierId ? options.verifiers?.[verifierId] : void 0;
|
|
699
|
+
if (!verifier) {
|
|
700
|
+
return {
|
|
701
|
+
success: false,
|
|
702
|
+
optionId: option.id,
|
|
703
|
+
attemptType: attempt.type,
|
|
704
|
+
failure: paymentFailureSchema.parse({
|
|
705
|
+
reason: `No verifier registered for id: ${verifierId}`,
|
|
706
|
+
code: "verifier_not_found",
|
|
707
|
+
retryable: false
|
|
708
|
+
})
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
const context = {
|
|
712
|
+
attempt,
|
|
713
|
+
option,
|
|
714
|
+
definition
|
|
715
|
+
};
|
|
716
|
+
if (options.settle !== void 0) {
|
|
717
|
+
context.settle = options.settle;
|
|
718
|
+
}
|
|
719
|
+
return verifier(context);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
success: false,
|
|
724
|
+
optionId: "",
|
|
725
|
+
attemptType: attempts[0]?.type ?? "direct",
|
|
726
|
+
failure: paymentFailureSchema.parse({
|
|
727
|
+
reason: "No matching payment option for attempt",
|
|
728
|
+
code: "unsupported_option",
|
|
729
|
+
retryable: false
|
|
730
|
+
})
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
function createPaymentResponseHeader(metadata) {
|
|
734
|
+
const parsed = paymentSuccessMetadataSchema.parse(metadata);
|
|
735
|
+
return encodeJson(parsed);
|
|
736
|
+
}
|
|
737
|
+
function parseX402Header(value) {
|
|
738
|
+
try {
|
|
739
|
+
const payload = decodeJson(value, x402PaymentHeaderSchema);
|
|
740
|
+
return {
|
|
741
|
+
attempt: {
|
|
742
|
+
type: "x402",
|
|
743
|
+
headerName: HEADER_X402,
|
|
744
|
+
raw: value,
|
|
745
|
+
payload
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
} catch (error) {
|
|
749
|
+
return {
|
|
750
|
+
failure: paymentFailureSchema.parse({
|
|
751
|
+
reason: `Invalid X-PAYMENT header: ${error.message}`,
|
|
752
|
+
code: "invalid_payload",
|
|
753
|
+
retryable: false
|
|
754
|
+
})
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function parseDirectHeader(value) {
|
|
759
|
+
try {
|
|
760
|
+
const payload = decodeJson(value, directPaymentPayloadSchema);
|
|
761
|
+
return {
|
|
762
|
+
attempt: {
|
|
763
|
+
type: "direct",
|
|
764
|
+
headerName: HEADER_DIRECT,
|
|
765
|
+
raw: value,
|
|
766
|
+
payload
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
} catch (error) {
|
|
770
|
+
return {
|
|
771
|
+
failure: paymentFailureSchema.parse({
|
|
772
|
+
reason: `Invalid X-PAYMENT-PROOF header: ${error.message}`,
|
|
773
|
+
code: "invalid_payload",
|
|
774
|
+
retryable: false
|
|
775
|
+
})
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function findMatchingOption(definition, attempt) {
|
|
780
|
+
return definition.accepts.find((candidate) => {
|
|
781
|
+
const option = paymentOptionSchema.parse(candidate);
|
|
782
|
+
if (attempt.type === "x402" && option.proof.mode === "x402") {
|
|
783
|
+
return option.proof.scheme === attempt.payload.scheme && option.proof.network === attempt.payload.network;
|
|
784
|
+
}
|
|
785
|
+
if (attempt.type === "direct" && option.proof.mode === "direct") {
|
|
786
|
+
return option.id === attempt.payload.optionId;
|
|
787
|
+
}
|
|
788
|
+
return false;
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
function resolveX402Version(options) {
|
|
792
|
+
const versions = [];
|
|
793
|
+
for (const option of options) {
|
|
794
|
+
if (option.proof.mode === "x402" && option.proof.version) {
|
|
795
|
+
versions.push(option.proof.version);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return versions.length > 0 ? Math.max(...versions) : X402_VERSION_DEFAULT;
|
|
799
|
+
}
|
|
800
|
+
function toX402Requirement(option, fallbackResource, settlement) {
|
|
801
|
+
if (option.proof.mode !== "x402") {
|
|
802
|
+
return void 0;
|
|
803
|
+
}
|
|
804
|
+
const decimals = resolveDecimals(option);
|
|
805
|
+
const assetAddress = option.asset.address;
|
|
806
|
+
if (!assetAddress) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
`x402 payment option '${option.id}' is missing asset.address`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
const units = decimalToBaseUnits(option.amount.value, decimals);
|
|
812
|
+
return x402RequirementSchema.parse({
|
|
813
|
+
scheme: option.proof.scheme,
|
|
814
|
+
network: option.proof.network,
|
|
815
|
+
maxAmountRequired: units,
|
|
816
|
+
asset: assetAddress,
|
|
817
|
+
payTo: option.payTo,
|
|
818
|
+
resource: option.resource ?? fallbackResource,
|
|
819
|
+
description: option.description,
|
|
820
|
+
maxTimeoutSeconds: settlement?.windowSeconds,
|
|
821
|
+
extra: {
|
|
822
|
+
symbol: option.asset.symbol,
|
|
823
|
+
currencyCode: option.amount.currency.code,
|
|
824
|
+
decimals
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
function resolveDecimals(option) {
|
|
829
|
+
if (typeof option.asset.decimals === "number") {
|
|
830
|
+
return option.asset.decimals;
|
|
831
|
+
}
|
|
832
|
+
if (typeof option.amount.currency.decimals === "number") {
|
|
833
|
+
return option.amount.currency.decimals;
|
|
834
|
+
}
|
|
835
|
+
throw new Error(
|
|
836
|
+
`Payment option '${option.id}' must specify asset.decimals or currency.decimals`
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
function decimalToBaseUnits(value, decimals) {
|
|
840
|
+
const [whole, fraction = ""] = value.split(".");
|
|
841
|
+
const sanitizedFraction = fraction.slice(0, decimals);
|
|
842
|
+
const paddedFraction = sanitizedFraction.padEnd(decimals, "0");
|
|
843
|
+
const combined = `${whole}${paddedFraction}`.replace(/^0+/, "");
|
|
844
|
+
return combined.length > 0 ? combined : "0";
|
|
845
|
+
}
|
|
846
|
+
function decodeJson(value, schema) {
|
|
847
|
+
const base64 = normalizeBase64(value);
|
|
848
|
+
const json = Buffer.from(base64, "base64").toString("utf-8");
|
|
849
|
+
const parsed = JSON.parse(json);
|
|
850
|
+
return schema.parse(parsed);
|
|
851
|
+
}
|
|
852
|
+
function encodeJson(value) {
|
|
853
|
+
const json = JSON.stringify(value);
|
|
854
|
+
return Buffer.from(json, "utf-8").toString("base64");
|
|
855
|
+
}
|
|
856
|
+
function normalizeBase64(input) {
|
|
857
|
+
if (/^[A-Za-z0-9+/=]+$/.test(input)) {
|
|
858
|
+
return input;
|
|
859
|
+
}
|
|
860
|
+
const restored = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
861
|
+
const paddingNeeded = (4 - restored.length % 4) % 4;
|
|
862
|
+
return restored + "=".repeat(paddingNeeded);
|
|
863
|
+
}
|
|
864
|
+
async function runFacilitatorVerifier({
|
|
865
|
+
attempt,
|
|
866
|
+
option,
|
|
867
|
+
definition,
|
|
868
|
+
settle,
|
|
869
|
+
fetchImpl
|
|
870
|
+
}) {
|
|
871
|
+
if (option.proof.mode !== "x402" || attempt.type !== "x402" || !option.proof.facilitator) {
|
|
872
|
+
return {
|
|
873
|
+
success: false,
|
|
874
|
+
optionId: option.id,
|
|
875
|
+
attemptType: attempt.type,
|
|
876
|
+
failure: paymentFailureSchema.parse({
|
|
877
|
+
reason: "Facilitator verifier invoked for non-x402 option",
|
|
878
|
+
code: "verification_failed",
|
|
879
|
+
retryable: false
|
|
880
|
+
})
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
const facilitator = option.proof.facilitator;
|
|
884
|
+
const verifierUrl = new URL(
|
|
885
|
+
facilitator.verifyPath ?? "/verify",
|
|
886
|
+
ensureTrailingSlash(facilitator.url)
|
|
887
|
+
).toString();
|
|
888
|
+
const requirement = toX402Requirement(option, definition.resource, option.settlement);
|
|
889
|
+
if (!requirement) {
|
|
890
|
+
return {
|
|
891
|
+
success: false,
|
|
892
|
+
optionId: option.id,
|
|
893
|
+
attemptType: attempt.type,
|
|
894
|
+
failure: paymentFailureSchema.parse({
|
|
895
|
+
reason: "Unable to derive x402 requirement for facilitator",
|
|
896
|
+
code: "verification_failed",
|
|
897
|
+
retryable: false
|
|
898
|
+
})
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
const headers = buildFacilitatorHeaders(facilitator);
|
|
902
|
+
const controller = facilitator.timeoutMs ? new AbortController() : void 0;
|
|
903
|
+
const timeout = facilitator.timeoutMs ? setTimeout(() => controller?.abort(), facilitator.timeoutMs) : void 0;
|
|
904
|
+
try {
|
|
905
|
+
const verifyResponse = await fetchImpl(verifierUrl, {
|
|
906
|
+
method: "POST",
|
|
907
|
+
headers,
|
|
908
|
+
body: JSON.stringify({
|
|
909
|
+
x402Version: attempt.payload.x402Version,
|
|
910
|
+
paymentHeader: attempt.raw,
|
|
911
|
+
paymentRequirements: requirement
|
|
912
|
+
}),
|
|
913
|
+
signal: controller?.signal ?? null
|
|
914
|
+
});
|
|
915
|
+
if (!verifyResponse.ok) {
|
|
916
|
+
return {
|
|
917
|
+
success: false,
|
|
918
|
+
optionId: option.id,
|
|
919
|
+
attemptType: attempt.type,
|
|
920
|
+
failure: paymentFailureSchema.parse({
|
|
921
|
+
reason: `Facilitator verify request failed: ${verifyResponse.status}`,
|
|
922
|
+
code: "verification_failed",
|
|
923
|
+
retryable: verifyResponse.status >= 500
|
|
924
|
+
})
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
const verifyPayload = await verifyResponse.json();
|
|
928
|
+
if (!verifyPayload.isValid) {
|
|
929
|
+
return {
|
|
930
|
+
success: false,
|
|
931
|
+
optionId: option.id,
|
|
932
|
+
attemptType: attempt.type,
|
|
933
|
+
failure: paymentFailureSchema.parse({
|
|
934
|
+
reason: verifyPayload.invalidReason ?? "Facilitator verification failed",
|
|
935
|
+
code: "verification_failed",
|
|
936
|
+
retryable: false
|
|
937
|
+
})
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
if (!settle) {
|
|
941
|
+
return {
|
|
942
|
+
success: true,
|
|
943
|
+
optionId: option.id,
|
|
944
|
+
attemptType: attempt.type,
|
|
945
|
+
metadata: paymentSuccessMetadataSchema.parse({
|
|
946
|
+
optionId: option.id,
|
|
947
|
+
verifier: facilitator.vendor ?? "facilitator"
|
|
948
|
+
})
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
const settleUrl = new URL(
|
|
952
|
+
facilitator.settlePath ?? "/settle",
|
|
953
|
+
ensureTrailingSlash(facilitator.url)
|
|
954
|
+
).toString();
|
|
955
|
+
const settleResponse = await fetchImpl(settleUrl, {
|
|
956
|
+
method: "POST",
|
|
957
|
+
headers,
|
|
958
|
+
body: JSON.stringify({
|
|
959
|
+
x402Version: attempt.payload.x402Version,
|
|
960
|
+
paymentHeader: attempt.raw,
|
|
961
|
+
paymentRequirements: requirement
|
|
962
|
+
}),
|
|
963
|
+
signal: controller?.signal ?? null
|
|
964
|
+
});
|
|
965
|
+
if (!settleResponse.ok) {
|
|
966
|
+
return {
|
|
967
|
+
success: false,
|
|
968
|
+
optionId: option.id,
|
|
969
|
+
attemptType: attempt.type,
|
|
970
|
+
failure: paymentFailureSchema.parse({
|
|
971
|
+
reason: `Facilitator settle request failed: ${settleResponse.status}`,
|
|
972
|
+
code: "verification_failed",
|
|
973
|
+
retryable: settleResponse.status >= 500
|
|
974
|
+
})
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
const settlePayload = await settleResponse.json();
|
|
978
|
+
if (!settlePayload.success) {
|
|
979
|
+
return {
|
|
980
|
+
success: false,
|
|
981
|
+
optionId: option.id,
|
|
982
|
+
attemptType: attempt.type,
|
|
983
|
+
failure: paymentFailureSchema.parse({
|
|
984
|
+
reason: settlePayload.error ?? "Facilitator settlement failed",
|
|
985
|
+
code: "verification_failed",
|
|
986
|
+
retryable: false
|
|
987
|
+
})
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
const metadata = paymentSuccessMetadataSchema.parse({
|
|
991
|
+
optionId: option.id,
|
|
992
|
+
verifier: facilitator.vendor ?? "facilitator",
|
|
993
|
+
txHash: settlePayload.txHash ?? void 0,
|
|
994
|
+
networkId: settlePayload.networkId ?? void 0
|
|
995
|
+
});
|
|
996
|
+
return {
|
|
997
|
+
success: true,
|
|
998
|
+
optionId: option.id,
|
|
999
|
+
attemptType: attempt.type,
|
|
1000
|
+
metadata,
|
|
1001
|
+
responseHeaders: {
|
|
1002
|
+
[HEADER_PAYMENT_RESPONSE]: createPaymentResponseHeader(metadata)
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
return {
|
|
1007
|
+
success: false,
|
|
1008
|
+
optionId: option.id,
|
|
1009
|
+
attemptType: attempt.type,
|
|
1010
|
+
failure: paymentFailureSchema.parse({
|
|
1011
|
+
reason: `Facilitator request error: ${error.message}`,
|
|
1012
|
+
code: "verification_failed",
|
|
1013
|
+
retryable: false
|
|
1014
|
+
})
|
|
1015
|
+
};
|
|
1016
|
+
} finally {
|
|
1017
|
+
if (timeout) {
|
|
1018
|
+
clearTimeout(timeout);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function buildFacilitatorHeaders(config) {
|
|
1023
|
+
const headers = {
|
|
1024
|
+
"content-type": "application/json"
|
|
1025
|
+
};
|
|
1026
|
+
if (config?.headers) {
|
|
1027
|
+
Object.assign(headers, config.headers);
|
|
1028
|
+
}
|
|
1029
|
+
const apiKey = resolveFacilitatorApiKey(config);
|
|
1030
|
+
if (apiKey) {
|
|
1031
|
+
const headerName = config?.apiKeyHeader ?? "Authorization";
|
|
1032
|
+
headers[headerName] = apiKey;
|
|
1033
|
+
}
|
|
1034
|
+
return headers;
|
|
1035
|
+
}
|
|
1036
|
+
function resolveFacilitatorApiKey(config) {
|
|
1037
|
+
if (!config) {
|
|
1038
|
+
return void 0;
|
|
1039
|
+
}
|
|
1040
|
+
if (config.apiKey) {
|
|
1041
|
+
return config.apiKey;
|
|
1042
|
+
}
|
|
1043
|
+
if (config.apiKeyEnv && typeof process !== "undefined") {
|
|
1044
|
+
return process.env?.[config.apiKeyEnv];
|
|
1045
|
+
}
|
|
1046
|
+
return void 0;
|
|
1047
|
+
}
|
|
1048
|
+
function ensureTrailingSlash(value) {
|
|
1049
|
+
return value.endsWith("/") ? value : `${value}/`;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/payment/index.ts
|
|
1053
|
+
var PAYMENT_CONTEXT_SYMBOL = Symbol.for("opentool.payment.context");
|
|
1054
|
+
var PaymentRequiredError = class extends Error {
|
|
1055
|
+
constructor(response, verification) {
|
|
1056
|
+
super("Payment required");
|
|
1057
|
+
this.name = "PaymentRequiredError";
|
|
1058
|
+
this.response = response;
|
|
1059
|
+
this.verification = verification;
|
|
1060
|
+
}
|
|
16
1061
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.
|
|
47
|
-
.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
1062
|
+
function setPaymentContext(request, context) {
|
|
1063
|
+
try {
|
|
1064
|
+
Object.defineProperty(request, PAYMENT_CONTEXT_SYMBOL, {
|
|
1065
|
+
value: context,
|
|
1066
|
+
configurable: true,
|
|
1067
|
+
enumerable: false,
|
|
1068
|
+
writable: true
|
|
1069
|
+
});
|
|
1070
|
+
} catch {
|
|
1071
|
+
request[PAYMENT_CONTEXT_SYMBOL] = context;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
function applyPaymentHeaders(response, headers) {
|
|
1075
|
+
const entries = Object.entries(headers ?? {});
|
|
1076
|
+
if (entries.length === 0) {
|
|
1077
|
+
return response;
|
|
1078
|
+
}
|
|
1079
|
+
let mutated = false;
|
|
1080
|
+
const merged = new Headers(response.headers);
|
|
1081
|
+
for (const [key, value] of entries) {
|
|
1082
|
+
if (!merged.has(key)) {
|
|
1083
|
+
merged.set(key, value);
|
|
1084
|
+
mutated = true;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (!mutated) {
|
|
1088
|
+
return response;
|
|
1089
|
+
}
|
|
1090
|
+
return new Response(response.body, {
|
|
1091
|
+
status: response.status,
|
|
1092
|
+
statusText: response.statusText,
|
|
1093
|
+
headers: merged
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
function withPaymentRequirement(handler, payment, options = {}) {
|
|
1097
|
+
return async (request) => {
|
|
1098
|
+
const verification = await requirePayment(request, payment, options);
|
|
1099
|
+
if (verification instanceof Response) {
|
|
1100
|
+
return verification;
|
|
1101
|
+
}
|
|
1102
|
+
setPaymentContext(request, verification);
|
|
1103
|
+
const response = await Promise.resolve(handler(request));
|
|
1104
|
+
return applyPaymentHeaders(response, verification.headers);
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
async function requirePayment(request, payment, options = {}) {
|
|
1108
|
+
const { definition, verifiers } = normalizePayment(payment);
|
|
1109
|
+
const mergedVerifiers = {
|
|
1110
|
+
...verifiers,
|
|
1111
|
+
...options.verifiers ?? {}
|
|
1112
|
+
};
|
|
1113
|
+
const verifyOptions = {
|
|
1114
|
+
definition,
|
|
1115
|
+
request
|
|
1116
|
+
};
|
|
1117
|
+
if (Object.keys(mergedVerifiers).length > 0) {
|
|
1118
|
+
verifyOptions.verifiers = mergedVerifiers;
|
|
1119
|
+
}
|
|
1120
|
+
if (options.settle !== void 0) {
|
|
1121
|
+
verifyOptions.settle = options.settle;
|
|
1122
|
+
}
|
|
1123
|
+
if (options.fetchImpl) {
|
|
1124
|
+
verifyOptions.fetchImpl = options.fetchImpl;
|
|
1125
|
+
}
|
|
1126
|
+
const verification = await verifyPayment(verifyOptions);
|
|
1127
|
+
if (!verification.success || !verification.metadata) {
|
|
1128
|
+
if (options.onFailure) {
|
|
1129
|
+
return options.onFailure(verification);
|
|
1130
|
+
}
|
|
1131
|
+
const response = paymentRequiredResponse(definition);
|
|
1132
|
+
throw new PaymentRequiredError(response, verification);
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
payment: verification.metadata,
|
|
1136
|
+
headers: verification.responseHeaders ?? {},
|
|
1137
|
+
optionId: verification.optionId,
|
|
1138
|
+
result: verification
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function normalizePayment(payment) {
|
|
1142
|
+
if (isDefinedPayment(payment)) {
|
|
1143
|
+
return {
|
|
1144
|
+
definition: payment.definition,
|
|
1145
|
+
verifiers: payment.verifiers ?? {}
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
return {
|
|
1149
|
+
definition: payment,
|
|
1150
|
+
verifiers: {}
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
function isDefinedPayment(value) {
|
|
1154
|
+
return !!value && typeof value === "object" && "definition" in value && value.definition !== void 0;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/adapters/mcp.ts
|
|
1158
|
+
function createMcpAdapter(options) {
|
|
1159
|
+
const normalizedSchema = ensureSchema(options.schema);
|
|
1160
|
+
const defaultMethod = resolveDefaultMethod(options);
|
|
1161
|
+
const httpHandler = options.httpHandlers[defaultMethod];
|
|
1162
|
+
if (!httpHandler) {
|
|
1163
|
+
throw new Error(
|
|
1164
|
+
`Tool "${options.name}" does not export an HTTP handler for ${defaultMethod}`
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
return async function invoke(rawArguments) {
|
|
1168
|
+
const validated = normalizedSchema ? normalizedSchema.parse(rawArguments ?? {}) : rawArguments;
|
|
1169
|
+
const request = buildRequest(options.name, defaultMethod, validated);
|
|
1170
|
+
try {
|
|
1171
|
+
const response = await Promise.resolve(httpHandler(request));
|
|
1172
|
+
return await responseToToolResponse(response);
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
if (error instanceof PaymentRequiredError) {
|
|
1175
|
+
return await responseToToolResponse(error.response);
|
|
1176
|
+
}
|
|
1177
|
+
throw error;
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
function resolveDefaultMethod(options) {
|
|
1182
|
+
const explicit = options.defaultMethod?.toUpperCase();
|
|
1183
|
+
if (explicit && typeof options.httpHandlers[explicit] === "function") {
|
|
1184
|
+
return explicit;
|
|
1185
|
+
}
|
|
1186
|
+
const preferredOrder = ["POST", "PUT", "PATCH", "GET", "DELETE", "OPTIONS", "HEAD"];
|
|
1187
|
+
for (const method of preferredOrder) {
|
|
1188
|
+
if (typeof options.httpHandlers[method] === "function") {
|
|
1189
|
+
return method;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const available = Object.keys(options.httpHandlers).filter(
|
|
1193
|
+
(method) => typeof options.httpHandlers[method] === "function"
|
|
1194
|
+
);
|
|
1195
|
+
if (available.length > 0) {
|
|
1196
|
+
return available[0];
|
|
1197
|
+
}
|
|
1198
|
+
throw new Error(`No HTTP handlers available for tool "${options.name}"`);
|
|
1199
|
+
}
|
|
1200
|
+
function ensureSchema(schema) {
|
|
1201
|
+
if (!schema) {
|
|
1202
|
+
return void 0;
|
|
1203
|
+
}
|
|
1204
|
+
if (schema instanceof z.ZodType) {
|
|
1205
|
+
return schema;
|
|
1206
|
+
}
|
|
1207
|
+
if (typeof schema?.parse === "function") {
|
|
1208
|
+
return schema;
|
|
1209
|
+
}
|
|
1210
|
+
throw new Error("MCP adapter requires a valid Zod schema to validate arguments");
|
|
1211
|
+
}
|
|
1212
|
+
function buildRequest(name, method, params) {
|
|
1213
|
+
const url = new URL(`https://opentool.local/${encodeURIComponent(name)}`);
|
|
1214
|
+
const headers = new Headers({
|
|
1215
|
+
"x-opentool-invocation": "mcp",
|
|
1216
|
+
"x-opentool-tool": name
|
|
1217
|
+
});
|
|
1218
|
+
if (method === "GET" || method === "HEAD") {
|
|
1219
|
+
if (params && typeof params === "object") {
|
|
1220
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
1221
|
+
if (value == null) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
url.searchParams.set(key, String(value));
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
return new Request(url, { method, headers });
|
|
1228
|
+
}
|
|
1229
|
+
headers.set("Content-Type", "application/json");
|
|
1230
|
+
const init = { method, headers };
|
|
1231
|
+
if (params != null) {
|
|
1232
|
+
init.body = JSON.stringify(params);
|
|
1233
|
+
}
|
|
1234
|
+
return new Request(url, init);
|
|
1235
|
+
}
|
|
1236
|
+
async function responseToToolResponse(response) {
|
|
1237
|
+
const statusIsError = response.status >= 400;
|
|
1238
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1239
|
+
const text = await response.text();
|
|
1240
|
+
if (contentType.includes("application/json")) {
|
|
1241
|
+
try {
|
|
1242
|
+
const payload = text ? JSON.parse(text) : {};
|
|
1243
|
+
if (payload && typeof payload === "object" && Array.isArray(payload.content)) {
|
|
1244
|
+
return {
|
|
1245
|
+
content: payload.content,
|
|
1246
|
+
isError: payload.isError ?? statusIsError
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
return {
|
|
1250
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
1251
|
+
isError: statusIsError
|
|
1252
|
+
};
|
|
1253
|
+
} catch {
|
|
1254
|
+
return {
|
|
1255
|
+
content: [{ type: "text", text }],
|
|
1256
|
+
isError: statusIsError
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (!text) {
|
|
1261
|
+
return {
|
|
1262
|
+
content: [],
|
|
1263
|
+
isError: statusIsError
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
return {
|
|
1267
|
+
content: [{ type: "text", text }],
|
|
1268
|
+
isError: statusIsError
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// src/types/index.ts
|
|
1273
|
+
var HTTP_METHODS = [
|
|
1274
|
+
"GET",
|
|
1275
|
+
"HEAD",
|
|
1276
|
+
"POST",
|
|
1277
|
+
"PUT",
|
|
1278
|
+
"DELETE",
|
|
1279
|
+
"PATCH",
|
|
1280
|
+
"OPTIONS"
|
|
1281
|
+
];
|
|
1282
|
+
|
|
1283
|
+
// src/cli/validate.ts
|
|
1284
|
+
var SUPPORTED_EXTENSIONS = [
|
|
1285
|
+
".ts",
|
|
1286
|
+
".tsx",
|
|
1287
|
+
".js",
|
|
1288
|
+
".jsx",
|
|
1289
|
+
".mjs",
|
|
1290
|
+
".cjs"
|
|
1291
|
+
];
|
|
1292
|
+
async function validateCommand(options) {
|
|
1293
|
+
console.log("\u{1F50D} Validating OpenTool metadata...");
|
|
1294
|
+
try {
|
|
1295
|
+
const toolsDir = path5.resolve(options.input);
|
|
1296
|
+
if (!fs4.existsSync(toolsDir)) {
|
|
1297
|
+
throw new Error(`Tools directory not found: ${toolsDir}`);
|
|
1298
|
+
}
|
|
1299
|
+
const projectRoot = path5.dirname(toolsDir);
|
|
1300
|
+
const tools = await loadAndValidateTools(toolsDir, { projectRoot });
|
|
1301
|
+
if (tools.length === 0) {
|
|
1302
|
+
throw new Error("No valid tools found - metadata validation aborted");
|
|
1303
|
+
}
|
|
1304
|
+
const { metadata, defaultsApplied, sourceMetadataPath } = await buildMetadataArtifact({
|
|
1305
|
+
projectRoot,
|
|
1306
|
+
tools
|
|
1307
|
+
});
|
|
1308
|
+
logMetadataSummary(metadata, defaultsApplied, sourceMetadataPath);
|
|
1309
|
+
console.log("\n\u2705 Metadata validation passed!\n");
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
console.error("\u274C Metadata validation failed:", error);
|
|
1312
|
+
process.exit(1);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
async function validateFullCommand(options) {
|
|
1316
|
+
console.log("\u{1F50D} Running full OpenTool validation...\n");
|
|
1317
|
+
try {
|
|
1318
|
+
const toolsDir = path5.resolve(options.input);
|
|
1319
|
+
if (!fs4.existsSync(toolsDir)) {
|
|
1320
|
+
throw new Error(`Tools directory not found: ${toolsDir}`);
|
|
1321
|
+
}
|
|
1322
|
+
const projectRoot = path5.dirname(toolsDir);
|
|
1323
|
+
const tools = await loadAndValidateTools(toolsDir, { projectRoot });
|
|
1324
|
+
if (tools.length === 0) {
|
|
1325
|
+
throw new Error("No tools discovered in the target directory");
|
|
1326
|
+
}
|
|
1327
|
+
const names = tools.map((tool) => tool.metadata?.name ?? tool.filename);
|
|
1328
|
+
const duplicates = findDuplicates(names);
|
|
1329
|
+
if (duplicates.length > 0) {
|
|
1330
|
+
throw new Error(`Duplicate tool names found: ${duplicates.join(", ")}`);
|
|
1331
|
+
}
|
|
1332
|
+
const { metadata, defaultsApplied, sourceMetadataPath } = await buildMetadataArtifact({
|
|
1333
|
+
projectRoot,
|
|
1334
|
+
tools
|
|
1335
|
+
});
|
|
1336
|
+
console.log(`\u{1F4E6} Tools loaded: ${tools.length}`);
|
|
1337
|
+
tools.forEach((tool) => {
|
|
1338
|
+
const toolName = tool.metadata?.name ?? tool.filename;
|
|
1339
|
+
const description = tool.metadata?.description ?? `${toolName} tool`;
|
|
1340
|
+
console.log(` \u2022 ${toolName} \u2014 ${description}`);
|
|
1341
|
+
});
|
|
1342
|
+
logMetadataSummary(metadata, defaultsApplied, sourceMetadataPath);
|
|
1343
|
+
console.log("\n\u2705 Full validation completed successfully\n");
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
console.error("\u274C Full validation failed:", error);
|
|
1346
|
+
process.exit(1);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
async function loadAndValidateTools(toolsDir, options = {}) {
|
|
1350
|
+
const files = fs4.readdirSync(toolsDir).filter((file) => SUPPORTED_EXTENSIONS.includes(path5.extname(file)));
|
|
1351
|
+
if (files.length === 0) {
|
|
1352
|
+
return [];
|
|
1353
|
+
}
|
|
1354
|
+
const projectRoot = options.projectRoot ?? path5.dirname(toolsDir);
|
|
1355
|
+
const tempDir = path5.join(toolsDir, ".opentool-temp");
|
|
1356
|
+
if (fs4.existsSync(tempDir)) {
|
|
1357
|
+
fs4.rmSync(tempDir, { recursive: true, force: true });
|
|
1358
|
+
}
|
|
1359
|
+
const entryPoints = files.map((file) => path5.join(toolsDir, file));
|
|
1360
|
+
const { outDir, cleanup } = await transpileWithEsbuild({
|
|
1361
|
+
entryPoints,
|
|
1362
|
+
projectRoot,
|
|
1363
|
+
format: "esm",
|
|
1364
|
+
outDir: tempDir
|
|
1365
|
+
});
|
|
1366
|
+
const tools = [];
|
|
1367
|
+
try {
|
|
1368
|
+
for (const file of files) {
|
|
1369
|
+
const compiledPath = resolveCompiledPath(outDir, file);
|
|
1370
|
+
if (!fs4.existsSync(compiledPath)) {
|
|
1371
|
+
throw new Error(`Failed to compile ${file}`);
|
|
1372
|
+
}
|
|
1373
|
+
const moduleExports = await importFresh(compiledPath);
|
|
1374
|
+
const toolModule = extractToolModule(moduleExports, file);
|
|
1375
|
+
const schema = ensureZodSchema(toolModule.schema, file);
|
|
1376
|
+
const paymentExport = toolModule.payment;
|
|
1377
|
+
const toolName = toolModule.metadata?.name ?? toolModule.metadata?.title ?? toBaseName(file);
|
|
1378
|
+
const inputSchemaRaw = schema ? toJsonSchema(toolName, schema) : void 0;
|
|
1379
|
+
const inputSchema = normalizeInputSchema(inputSchemaRaw);
|
|
1380
|
+
const httpHandlersRaw = collectHttpHandlers(toolModule, file);
|
|
1381
|
+
const httpHandlers = [...httpHandlersRaw];
|
|
1382
|
+
if (httpHandlers.length === 0) {
|
|
1383
|
+
throw new Error(
|
|
1384
|
+
`${file} must export at least one HTTP handler (e.g. POST)`
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
if (paymentExport) {
|
|
1388
|
+
for (let index = 0; index < httpHandlers.length; index += 1) {
|
|
1389
|
+
const entry = httpHandlers[index];
|
|
1390
|
+
httpHandlers[index] = {
|
|
1391
|
+
...entry,
|
|
1392
|
+
handler: withPaymentRequirement(entry.handler, paymentExport)
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const httpHandlerMap = toHttpHandlerMap(httpHandlers);
|
|
1397
|
+
const defaultMethod = typeof toolModule.mcp?.defaultMethod === "string" ? toolModule.mcp.defaultMethod : void 0;
|
|
1398
|
+
const adapter = createMcpAdapter({
|
|
1399
|
+
name: toolName,
|
|
1400
|
+
httpHandlers: httpHandlerMap,
|
|
1401
|
+
...defaultMethod ? { defaultMethod } : {},
|
|
1402
|
+
...schema ? { schema } : {}
|
|
1403
|
+
});
|
|
1404
|
+
let metadataOverrides = toolModule.metadata ?? null;
|
|
1405
|
+
if (paymentExport?.metadata) {
|
|
1406
|
+
if (metadataOverrides) {
|
|
1407
|
+
metadataOverrides = {
|
|
1408
|
+
...metadataOverrides,
|
|
1409
|
+
payment: metadataOverrides.payment ?? paymentExport.metadata,
|
|
1410
|
+
annotations: {
|
|
1411
|
+
...metadataOverrides.annotations ?? {},
|
|
1412
|
+
requiresPayment: metadataOverrides.annotations?.requiresPayment ?? true
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
} else {
|
|
1416
|
+
metadataOverrides = {
|
|
1417
|
+
payment: paymentExport.metadata,
|
|
1418
|
+
annotations: { requiresPayment: true }
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
const tool = {
|
|
1423
|
+
schema: schema ?? void 0,
|
|
1424
|
+
inputSchema,
|
|
1425
|
+
metadata: metadataOverrides,
|
|
1426
|
+
httpHandlers,
|
|
1427
|
+
mcpConfig: normalizeMcpConfig(toolModule.mcp, file),
|
|
1428
|
+
filename: toBaseName(file),
|
|
1429
|
+
sourcePath: path5.join(toolsDir, file),
|
|
1430
|
+
handler: async (params) => adapter(params),
|
|
1431
|
+
payment: paymentExport ?? null
|
|
1432
|
+
};
|
|
1433
|
+
tools.push(tool);
|
|
1434
|
+
}
|
|
1435
|
+
} finally {
|
|
1436
|
+
cleanup();
|
|
1437
|
+
if (fs4.existsSync(tempDir)) {
|
|
1438
|
+
fs4.rmSync(tempDir, { recursive: true, force: true });
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return tools;
|
|
1442
|
+
}
|
|
1443
|
+
function extractToolModule(exportsObject, filename) {
|
|
1444
|
+
const candidates = [exportsObject, exportsObject?.default];
|
|
1445
|
+
for (const candidate of candidates) {
|
|
1446
|
+
if (candidate && typeof candidate === "object") {
|
|
1447
|
+
const hasSchema = candidate.schema && typeof candidate.schema === "object";
|
|
1448
|
+
const hasHttp = HTTP_METHODS.some((method) => typeof candidate[method] === "function");
|
|
1449
|
+
if (hasSchema || hasHttp) {
|
|
1450
|
+
return candidate;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
throw new Error(
|
|
1455
|
+
`${filename} must export a tool definition. Expected a Zod schema plus HTTP handlers (export async function POST).`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
function toJsonSchema(name, schema) {
|
|
1459
|
+
if (!schema) {
|
|
1460
|
+
return void 0;
|
|
1461
|
+
}
|
|
1462
|
+
try {
|
|
1463
|
+
return zodToJsonSchema(schema, {
|
|
1464
|
+
name: `${name}Schema`,
|
|
1465
|
+
target: "jsonSchema7",
|
|
1466
|
+
$refStrategy: "none"
|
|
1467
|
+
});
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
throw new Error(`Failed to convert Zod schema for ${name}: ${error}`);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
function toBaseName(file) {
|
|
1473
|
+
return file.replace(/\.[^.]+$/, "");
|
|
1474
|
+
}
|
|
1475
|
+
function ensureZodSchema(schemaCandidate, filename) {
|
|
1476
|
+
if (schemaCandidate == null) {
|
|
1477
|
+
return void 0;
|
|
1478
|
+
}
|
|
1479
|
+
if (schemaCandidate instanceof z.ZodType) {
|
|
1480
|
+
return schemaCandidate;
|
|
1481
|
+
}
|
|
1482
|
+
const schema = schemaCandidate;
|
|
1483
|
+
if (typeof schema?.parse !== "function") {
|
|
1484
|
+
throw new Error(`${filename} schema export must be a Zod schema (missing parse method)`);
|
|
1485
|
+
}
|
|
1486
|
+
return schema;
|
|
1487
|
+
}
|
|
1488
|
+
function collectHttpHandlers(module, filename) {
|
|
1489
|
+
const handlers = [];
|
|
1490
|
+
for (const method of HTTP_METHODS) {
|
|
1491
|
+
const handler = module?.[method];
|
|
1492
|
+
if (typeof handler === "function") {
|
|
1493
|
+
handlers.push({
|
|
1494
|
+
method,
|
|
1495
|
+
handler: async (request) => handler.call(module, request)
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
handlers.sort((a, b) => HTTP_METHODS.indexOf(a.method) - HTTP_METHODS.indexOf(b.method));
|
|
1500
|
+
const duplicates = findDuplicates(handlers.map((h) => h.method));
|
|
1501
|
+
if (duplicates.length > 0) {
|
|
1502
|
+
throw new Error(
|
|
1503
|
+
`${filename} exports multiple handlers for HTTP method(s): ${duplicates.join(", ")}`
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
return handlers;
|
|
1507
|
+
}
|
|
1508
|
+
function toHttpHandlerMap(handlers) {
|
|
1509
|
+
return handlers.reduce((acc, handler) => {
|
|
1510
|
+
acc[handler.method.toUpperCase()] = handler.handler;
|
|
1511
|
+
return acc;
|
|
1512
|
+
}, {});
|
|
1513
|
+
}
|
|
1514
|
+
function normalizeInputSchema(schema) {
|
|
1515
|
+
if (!schema || typeof schema !== "object") {
|
|
1516
|
+
return schema;
|
|
1517
|
+
}
|
|
1518
|
+
const clone = JSON.parse(JSON.stringify(schema));
|
|
1519
|
+
if (typeof clone.$ref === "string" && clone.$ref.startsWith("#/definitions/")) {
|
|
1520
|
+
const refName = clone.$ref.replace("#/definitions/", "");
|
|
1521
|
+
const definitions = clone.definitions;
|
|
1522
|
+
if (definitions && typeof definitions[refName] === "object") {
|
|
1523
|
+
return normalizeInputSchema(definitions[refName]);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
delete clone.$ref;
|
|
1527
|
+
delete clone.definitions;
|
|
1528
|
+
if (!("type" in clone)) {
|
|
1529
|
+
clone.type = "object";
|
|
1530
|
+
}
|
|
1531
|
+
return clone;
|
|
1532
|
+
}
|
|
1533
|
+
function normalizeMcpConfig(rawConfig, filename) {
|
|
1534
|
+
if (rawConfig == null) {
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
if (rawConfig === false) {
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
if (rawConfig === true) {
|
|
1541
|
+
return { enabled: true };
|
|
1542
|
+
}
|
|
1543
|
+
if (!isPlainObject(rawConfig)) {
|
|
1544
|
+
throw new Error(`${filename} export \\"mcp\\" must be an object with an enabled flag`);
|
|
1545
|
+
}
|
|
1546
|
+
const enabledRaw = rawConfig.enabled;
|
|
1547
|
+
if (enabledRaw === false) {
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
if (enabledRaw !== true) {
|
|
1551
|
+
throw new Error(`${filename} mcp.enabled must be explicitly set to true to opt-in to MCP`);
|
|
1552
|
+
}
|
|
1553
|
+
const modeRaw = rawConfig.mode;
|
|
1554
|
+
let mode;
|
|
1555
|
+
if (typeof modeRaw === "string") {
|
|
1556
|
+
const normalized = modeRaw.toLowerCase();
|
|
1557
|
+
if (["stdio", "lambda", "dual"].includes(normalized)) {
|
|
1558
|
+
mode = normalized;
|
|
1559
|
+
} else {
|
|
1560
|
+
throw new Error(
|
|
1561
|
+
`${filename} mcp.mode must be one of "stdio", "lambda", or "dual" if specified`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
const defaultMethodRaw = rawConfig.defaultMethod;
|
|
1566
|
+
const defaultMethod = typeof defaultMethodRaw === "string" ? defaultMethodRaw.toUpperCase() : void 0;
|
|
1567
|
+
const overridesRaw = rawConfig.metadataOverrides;
|
|
1568
|
+
const metadataOverrides = isPlainObject(overridesRaw) ? overridesRaw : void 0;
|
|
1569
|
+
const config = {
|
|
1570
|
+
enabled: true
|
|
1571
|
+
};
|
|
1572
|
+
if (mode) {
|
|
1573
|
+
config.mode = mode;
|
|
1574
|
+
}
|
|
1575
|
+
if (defaultMethod) {
|
|
1576
|
+
config.defaultMethod = defaultMethod;
|
|
1577
|
+
}
|
|
1578
|
+
if (metadataOverrides) {
|
|
1579
|
+
config.metadataOverrides = metadataOverrides;
|
|
1580
|
+
}
|
|
1581
|
+
return config;
|
|
1582
|
+
}
|
|
1583
|
+
function isPlainObject(value) {
|
|
1584
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1585
|
+
}
|
|
1586
|
+
function findDuplicates(values) {
|
|
1587
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1588
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
1589
|
+
values.forEach((value) => {
|
|
1590
|
+
const count = seen.get(value) ?? 0;
|
|
1591
|
+
seen.set(value, count + 1);
|
|
1592
|
+
if (count >= 1) {
|
|
1593
|
+
duplicates.add(value);
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
return Array.from(duplicates.values());
|
|
1597
|
+
}
|
|
1598
|
+
function logMetadataSummary(metadata, defaultsApplied, sourceMetadataPath) {
|
|
1599
|
+
console.log(`\u{1F4C4} metadata loaded from ${sourceMetadataPath}`);
|
|
1600
|
+
console.log("\n\u{1F4CA} Metadata Summary:");
|
|
1601
|
+
console.log(` \u2022 Name: ${metadata.name}`);
|
|
1602
|
+
console.log(` \u2022 Display Name: ${metadata.displayName}`);
|
|
1603
|
+
console.log(` \u2022 Version: ${metadata.version}`);
|
|
1604
|
+
console.log(` \u2022 Category: ${metadata.category}`);
|
|
1605
|
+
console.log(` \u2022 Tools: ${metadata.tools.length}`);
|
|
1606
|
+
console.log(` \u2022 Spec Version: ${metadata.metadataSpecVersion}`);
|
|
1607
|
+
if (metadata.payment) {
|
|
1608
|
+
console.log(` \u2022 Payment: $${metadata.payment.amountUSDC} USDC`);
|
|
1609
|
+
}
|
|
1610
|
+
if (defaultsApplied.length > 0) {
|
|
1611
|
+
console.log("\nDefaults applied during metadata synthesis:");
|
|
1612
|
+
defaultsApplied.forEach((entry) => console.log(` \u2022 ${entry}`));
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// src/cli/build.ts
|
|
1617
|
+
async function buildCommand(options) {
|
|
1618
|
+
const start = timestamp();
|
|
1619
|
+
console.log(`[${start}] Building OpenTool project...`);
|
|
1620
|
+
try {
|
|
1621
|
+
const artifacts = await buildProject(options);
|
|
1622
|
+
logBuildSummary(artifacts, options);
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
const end = timestamp();
|
|
1625
|
+
console.error(`[${end}] Build failed:`, error);
|
|
1626
|
+
process.exit(1);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
async function buildProject(options) {
|
|
1630
|
+
const toolsDir = path5.resolve(options.input);
|
|
1631
|
+
if (!fs4.existsSync(toolsDir)) {
|
|
1632
|
+
throw new Error(`Tools directory not found: ${toolsDir}`);
|
|
1633
|
+
}
|
|
1634
|
+
const projectRoot = path5.dirname(toolsDir);
|
|
1635
|
+
const outputDir = path5.resolve(options.output);
|
|
1636
|
+
fs4.mkdirSync(outputDir, { recursive: true });
|
|
1637
|
+
const serverName = options.name ?? "opentool-server";
|
|
1638
|
+
const serverVersion = options.version ?? "1.0.0";
|
|
1639
|
+
const tools = await loadAndValidateTools(toolsDir, { projectRoot });
|
|
1640
|
+
if (tools.length === 0) {
|
|
1641
|
+
throw new Error("No valid tools found - build aborted");
|
|
1642
|
+
}
|
|
1643
|
+
const { metadata, defaultsApplied } = await buildMetadataArtifact({
|
|
1644
|
+
projectRoot,
|
|
1645
|
+
tools
|
|
1646
|
+
});
|
|
1647
|
+
const metadataPath = path5.join(outputDir, "metadata.json");
|
|
1648
|
+
fs4.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
|
1649
|
+
const compiledTools = await emitTools(tools, {
|
|
1650
|
+
projectRoot,
|
|
1651
|
+
outputDir
|
|
1652
|
+
});
|
|
1653
|
+
const shouldBuildMcpServer = compiledTools.some((artifact) => artifact.mcpEnabled);
|
|
1654
|
+
if (shouldBuildMcpServer) {
|
|
1655
|
+
await writeMcpServer({
|
|
1656
|
+
outputDir,
|
|
1657
|
+
serverName,
|
|
1658
|
+
serverVersion,
|
|
1659
|
+
compiledTools});
|
|
1660
|
+
} else {
|
|
1661
|
+
const serverPath = path5.join(outputDir, "mcp-server.js");
|
|
1662
|
+
if (fs4.existsSync(serverPath)) {
|
|
1663
|
+
fs4.rmSync(serverPath);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
return {
|
|
1667
|
+
metadata,
|
|
1668
|
+
defaultsApplied,
|
|
1669
|
+
tools,
|
|
1670
|
+
compiledTools
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
async function emitTools(tools, config) {
|
|
1674
|
+
const toolsOutDir = path5.join(config.outputDir, "tools");
|
|
1675
|
+
if (fs4.existsSync(toolsOutDir)) {
|
|
1676
|
+
fs4.rmSync(toolsOutDir, { recursive: true, force: true });
|
|
1677
|
+
}
|
|
1678
|
+
fs4.mkdirSync(toolsOutDir, { recursive: true });
|
|
1679
|
+
const entryPoints = tools.map((tool) => {
|
|
1680
|
+
if (!tool.sourcePath) {
|
|
1681
|
+
throw new Error(`Missing sourcePath for tool ${tool.filename}`);
|
|
1682
|
+
}
|
|
1683
|
+
return tool.sourcePath;
|
|
1684
|
+
});
|
|
1685
|
+
await transpileWithEsbuild({
|
|
1686
|
+
entryPoints,
|
|
1687
|
+
projectRoot: config.projectRoot,
|
|
1688
|
+
format: "cjs",
|
|
1689
|
+
outDir: toolsOutDir
|
|
1690
|
+
});
|
|
1691
|
+
const compiled = tools.map((tool) => {
|
|
1692
|
+
if (!tool.sourcePath) {
|
|
1693
|
+
throw new Error(`Missing sourcePath for tool ${tool.filename}`);
|
|
1694
|
+
}
|
|
1695
|
+
const base = path5.basename(tool.sourcePath).replace(/\.[^.]+$/, "");
|
|
1696
|
+
const modulePath = path5.join("tools", `${base}.js`);
|
|
1697
|
+
if (!fs4.existsSync(path5.join(config.outputDir, modulePath))) {
|
|
1698
|
+
throw new Error(`Expected compiled output missing: ${modulePath}`);
|
|
1699
|
+
}
|
|
1700
|
+
const defaultMcpMethod = tool.mcpConfig?.defaultMethod;
|
|
1701
|
+
return {
|
|
1702
|
+
name: tool.metadata?.name ?? tool.filename,
|
|
1703
|
+
filename: base,
|
|
1704
|
+
modulePath,
|
|
1705
|
+
httpMethods: tool.httpHandlers.map((handler) => handler.method),
|
|
1706
|
+
mcpEnabled: tool.mcpConfig?.enabled ?? false,
|
|
1707
|
+
...defaultMcpMethod ? { defaultMcpMethod } : {},
|
|
1708
|
+
hasWallet: Boolean(tool.payment)
|
|
1709
|
+
};
|
|
1710
|
+
});
|
|
1711
|
+
return compiled;
|
|
1712
|
+
}
|
|
1713
|
+
function renderMcpServer(options) {
|
|
1714
|
+
const toolImports = options.compiledTools.map((tool, index) => `const tool${index} = require('./${tool.modulePath}');`).join("\n");
|
|
1715
|
+
const registry = options.compiledTools.map((artifact, index) => {
|
|
1716
|
+
const config = {
|
|
1717
|
+
enabled: artifact.mcpEnabled,
|
|
1718
|
+
defaultMethod: artifact.defaultMcpMethod ?? null,
|
|
1719
|
+
httpMethods: artifact.httpMethods,
|
|
1720
|
+
filename: artifact.filename
|
|
1721
|
+
};
|
|
1722
|
+
return ` { meta: metadata.tools[${index}], module: tool${index}, config: ${JSON.stringify(config)} }`;
|
|
1723
|
+
}).join(",\n");
|
|
1724
|
+
return `#!/usr/bin/env node
|
|
1725
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
1726
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
1727
|
+
const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
|
|
1728
|
+
const metadata = require('./metadata.json');
|
|
1729
|
+
const { createMcpAdapter, HTTP_METHODS } = require('opentool/dist/adapters/mcp.js');
|
|
1730
|
+
|
|
1731
|
+
${toolImports}
|
|
1732
|
+
|
|
1733
|
+
const toolRegistry = [
|
|
1734
|
+
${registry}
|
|
1735
|
+
];
|
|
1736
|
+
|
|
1737
|
+
const adapters = toolRegistry.map((entry) => {
|
|
1738
|
+
if (!entry.config.enabled) {
|
|
1739
|
+
return null;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
const httpHandlers = Object.fromEntries(
|
|
1743
|
+
HTTP_METHODS
|
|
1744
|
+
.map((method) => [method, entry.module[method]])
|
|
1745
|
+
.filter(([, handler]) => typeof handler === 'function')
|
|
1746
|
+
);
|
|
1747
|
+
|
|
1748
|
+
return {
|
|
1749
|
+
meta: entry.meta,
|
|
1750
|
+
invoke: createMcpAdapter({
|
|
1751
|
+
name: entry.meta.name,
|
|
1752
|
+
schema: entry.module.schema,
|
|
1753
|
+
httpHandlers,
|
|
1754
|
+
defaultMethod: entry.config.defaultMethod || undefined,
|
|
1755
|
+
}),
|
|
1756
|
+
};
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
const server = new Server(
|
|
1760
|
+
{
|
|
1761
|
+
name: '${escapeForJs(options.serverName)}',
|
|
1762
|
+
version: '${escapeForJs(options.serverVersion)}',
|
|
1763
|
+
},
|
|
1764
|
+
{
|
|
1765
|
+
capabilities: {
|
|
1766
|
+
tools: {},
|
|
1767
|
+
},
|
|
1768
|
+
}
|
|
1769
|
+
);
|
|
1770
|
+
|
|
1771
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1772
|
+
tools: adapters
|
|
1773
|
+
.filter((entry) => entry !== null)
|
|
1774
|
+
.map((entry) => entry.meta),
|
|
1775
|
+
}));
|
|
1776
|
+
|
|
1777
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1778
|
+
const adapter = adapters.find((entry) => entry && entry.meta.name === request.params.name);
|
|
1779
|
+
if (!adapter) {
|
|
1780
|
+
throw new Error('Tool ' + request.params.name + ' is not registered for MCP');
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
try {
|
|
1784
|
+
return await adapter.invoke(request.params.arguments);
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
const message = (error && error.message) || String(error);
|
|
1787
|
+
return {
|
|
1788
|
+
content: [{ type: 'text', text: 'Error: ' + message }],
|
|
1789
|
+
isError: true,
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
async function main() {
|
|
1795
|
+
const transport = new StdioServerTransport();
|
|
1796
|
+
await server.connect(transport);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
if (require.main === module) {
|
|
1800
|
+
main().catch(console.error);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
module.exports = { server };
|
|
1804
|
+
`;
|
|
1805
|
+
}
|
|
1806
|
+
async function writeMcpServer(options) {
|
|
1807
|
+
const serverCode = renderMcpServer(options);
|
|
1808
|
+
const serverPath = path5.join(options.outputDir, "mcp-server.js");
|
|
1809
|
+
fs4.writeFileSync(serverPath, serverCode);
|
|
1810
|
+
fs4.chmodSync(serverPath, 493);
|
|
1811
|
+
}
|
|
1812
|
+
function logBuildSummary(artifacts, options) {
|
|
1813
|
+
const end = timestamp();
|
|
1814
|
+
console.log(`[${end}] Build completed successfully!`);
|
|
1815
|
+
console.log(`Output directory: ${path5.resolve(options.output)}`);
|
|
1816
|
+
console.log("Generated files:");
|
|
1817
|
+
const hasMcp = artifacts.compiledTools.some((tool) => tool.mcpEnabled);
|
|
1818
|
+
if (hasMcp) {
|
|
1819
|
+
console.log(" \u2022 mcp-server.js (stdio server)");
|
|
1820
|
+
}
|
|
1821
|
+
console.log(` \u2022 tools/ (${artifacts.compiledTools.length} compiled tools)`);
|
|
1822
|
+
artifacts.compiledTools.forEach((tool) => {
|
|
1823
|
+
const methods = tool.httpMethods.join(", ");
|
|
1824
|
+
const walletBadge = tool.hasWallet ? " [wallet]" : "";
|
|
1825
|
+
console.log(` - ${tool.name} [${methods}]${walletBadge}`);
|
|
1826
|
+
});
|
|
1827
|
+
console.log(" \u2022 metadata.json (registry artifact)");
|
|
1828
|
+
if (artifacts.defaultsApplied.length > 0) {
|
|
1829
|
+
console.log("\nDefaults applied during metadata synthesis:");
|
|
1830
|
+
artifacts.defaultsApplied.forEach((entry) => console.log(` \u2022 ${entry}`));
|
|
1831
|
+
}
|
|
1832
|
+
if (!hasMcp) {
|
|
1833
|
+
console.log("\n\u2139\uFE0F MCP adapter skipped (no tools opted in)");
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
function timestamp() {
|
|
1837
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
1838
|
+
}
|
|
1839
|
+
function escapeForJs(value) {
|
|
1840
|
+
return value.replace(/'/g, "\\'");
|
|
1841
|
+
}
|
|
1842
|
+
var __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
|
|
1843
|
+
var packageJson = JSON.parse(
|
|
1844
|
+
fs4.readFileSync(path5.resolve(__dirname2, "../../package.json"), "utf-8")
|
|
1845
|
+
);
|
|
1846
|
+
var cyan = "\x1B[36m";
|
|
1847
|
+
var bold = "\x1B[1m";
|
|
1848
|
+
var dim = "\x1B[2m";
|
|
1849
|
+
var reset = "\x1B[0m";
|
|
1850
|
+
async function devCommand(options) {
|
|
1851
|
+
const port = options.port ?? 7e3;
|
|
1852
|
+
const watch2 = options.watch ?? true;
|
|
1853
|
+
const enableStdio = options.stdio ?? false;
|
|
1854
|
+
const log = enableStdio ? (_message) => {
|
|
1855
|
+
} : (message) => console.log(message);
|
|
1856
|
+
try {
|
|
1857
|
+
const toolsDir = path5.resolve(options.input);
|
|
1858
|
+
if (!fs4.existsSync(toolsDir)) {
|
|
1859
|
+
throw new Error(`Tools directory not found: ${toolsDir}`);
|
|
1860
|
+
}
|
|
1861
|
+
const projectRoot = path5.dirname(toolsDir);
|
|
1862
|
+
let toolDefinitions = await loadToolDefinitions(toolsDir, projectRoot);
|
|
1863
|
+
if (toolDefinitions.length === 0) {
|
|
1864
|
+
throw new Error("No tools found in the target directory");
|
|
1865
|
+
}
|
|
1866
|
+
let routes = expandRoutes(toolDefinitions);
|
|
1867
|
+
const stdioController = enableStdio ? await startMcpServer(() => toolDefinitions) : null;
|
|
1868
|
+
if (watch2) {
|
|
1869
|
+
fs4.watch(toolsDir, async (_eventType, filename) => {
|
|
1870
|
+
if (filename && !/\.(ts|js|mjs|cjs|tsx|jsx)$/.test(filename)) {
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
log(
|
|
1874
|
+
`${dim}
|
|
1875
|
+
Detected change in ${filename ?? "tools directory"}, reloading...${reset}`
|
|
1876
|
+
);
|
|
1877
|
+
try {
|
|
1878
|
+
toolDefinitions = await loadToolDefinitions(toolsDir, projectRoot);
|
|
1879
|
+
routes = expandRoutes(toolDefinitions);
|
|
1880
|
+
logReload(toolDefinitions, enableStdio, log);
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
console.error("Failed to reload tools:", error);
|
|
1883
|
+
}
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
const server = http.createServer(async (req, res) => {
|
|
1887
|
+
const method = (req.method || "GET").toUpperCase();
|
|
1888
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
1889
|
+
const routePath = url.pathname;
|
|
1890
|
+
log(`${dim}[request] ${method} ${routePath}${reset}`);
|
|
1891
|
+
try {
|
|
1892
|
+
await handleRequest({ req, res, port, routes });
|
|
1893
|
+
log(
|
|
1894
|
+
`${dim}[response] ${method} ${routePath} ${res.statusCode}${reset}`
|
|
1895
|
+
);
|
|
1896
|
+
} catch (error) {
|
|
1897
|
+
console.error("Error handling request:", error);
|
|
1898
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1899
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
1900
|
+
log(`${dim}[response] ${method} ${routePath} 500${reset}`);
|
|
1901
|
+
}
|
|
1902
|
+
});
|
|
1903
|
+
server.listen(port, () => {
|
|
1904
|
+
log(`${bold}${dim}> dev opentool${reset}`);
|
|
1905
|
+
log(
|
|
1906
|
+
` * ${bold}opentool${reset} ${cyan}v${packageJson.version}${reset}`
|
|
1907
|
+
);
|
|
1908
|
+
log(` * ${bold}HTTP:${reset} http://localhost:${port}`);
|
|
1909
|
+
logStartup(toolDefinitions, enableStdio, log);
|
|
1910
|
+
});
|
|
1911
|
+
process.on("SIGINT", async () => {
|
|
1912
|
+
log(`
|
|
1913
|
+
${dim}Shutting down dev server...${reset}`);
|
|
1914
|
+
server.close();
|
|
1915
|
+
if (stdioController) {
|
|
1916
|
+
await stdioController.close();
|
|
1917
|
+
}
|
|
1918
|
+
process.exit(0);
|
|
1919
|
+
});
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
console.error("Dev server failed:", error);
|
|
1922
|
+
process.exit(1);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
async function startMcpServer(getTools) {
|
|
1926
|
+
const server = new Server(
|
|
1927
|
+
{
|
|
1928
|
+
name: "opentool-dev",
|
|
1929
|
+
version: "1.0.0"
|
|
1930
|
+
},
|
|
1931
|
+
{
|
|
1932
|
+
capabilities: {
|
|
1933
|
+
tools: {}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
);
|
|
1937
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1938
|
+
const tools = getTools().filter(isMcpEnabled);
|
|
1939
|
+
return {
|
|
1940
|
+
tools: tools.map((tool) => ({
|
|
1941
|
+
name: tool.metadata?.name ?? tool.filename,
|
|
1942
|
+
description: tool.metadata?.description ?? `${tool.filename} tool`,
|
|
1943
|
+
inputSchema: tool.inputSchema,
|
|
1944
|
+
annotations: tool.metadata?.annotations,
|
|
1945
|
+
payment: tool.metadata?.payment,
|
|
1946
|
+
discovery: tool.metadata?.discovery
|
|
1947
|
+
}))
|
|
1948
|
+
};
|
|
1949
|
+
});
|
|
1950
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1951
|
+
const tools = getTools().filter(isMcpEnabled);
|
|
1952
|
+
const tool = tools.find((entry) => {
|
|
1953
|
+
const toolName = entry.metadata?.name ?? entry.filename;
|
|
1954
|
+
return toolName === request.params.name;
|
|
1955
|
+
});
|
|
1956
|
+
if (!tool) {
|
|
1957
|
+
throw new Error(`Tool ${request.params.name} not found`);
|
|
1958
|
+
}
|
|
1959
|
+
try {
|
|
1960
|
+
const validatedParams = tool.schema.parse(
|
|
1961
|
+
request.params.arguments
|
|
1962
|
+
);
|
|
1963
|
+
const handler = tool.handler ?? createMcpAdapter({
|
|
1964
|
+
name: tool.metadata?.name ?? tool.filename,
|
|
1965
|
+
httpHandlers: toHttpHandlerMap2(tool.httpHandlers),
|
|
1966
|
+
...tool.schema ? { schema: tool.schema } : {},
|
|
1967
|
+
...tool.mcpConfig?.defaultMethod ? { defaultMethod: tool.mcpConfig.defaultMethod } : {}
|
|
1968
|
+
});
|
|
1969
|
+
const result = await handler(validatedParams);
|
|
1970
|
+
return result;
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
const message = error && error.message || String(error);
|
|
1973
|
+
return {
|
|
1974
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
1975
|
+
isError: true
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
const transport = new StdioServerTransport();
|
|
1980
|
+
server.connect(transport).catch((error) => {
|
|
1981
|
+
console.error("MCP transport error:", error);
|
|
1982
|
+
});
|
|
1983
|
+
return {
|
|
1984
|
+
async close() {
|
|
1985
|
+
await server.close();
|
|
1986
|
+
if (typeof transport.close === "function") {
|
|
1987
|
+
transport.close();
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
async function loadToolDefinitions(toolsDir, projectRoot) {
|
|
1993
|
+
return loadAndValidateTools(toolsDir, { projectRoot });
|
|
1994
|
+
}
|
|
1995
|
+
function expandRoutes(tools) {
|
|
1996
|
+
const routes = [];
|
|
1997
|
+
tools.forEach((tool) => {
|
|
1998
|
+
tool.httpHandlers.forEach((handlerDef) => {
|
|
1999
|
+
routes.push({
|
|
2000
|
+
tool,
|
|
2001
|
+
method: handlerDef.method.toUpperCase(),
|
|
2002
|
+
handler: async (request) => handlerDef.handler(request)
|
|
2003
|
+
});
|
|
2004
|
+
});
|
|
2005
|
+
});
|
|
2006
|
+
return routes;
|
|
2007
|
+
}
|
|
2008
|
+
function logStartup(tools, stdio, log) {
|
|
2009
|
+
log(`
|
|
2010
|
+
Tools: ${tools.length} tool${tools.length === 1 ? "" : "s"}`);
|
|
2011
|
+
printToolList(tools, log);
|
|
2012
|
+
if (stdio) {
|
|
2013
|
+
const mcpTools = tools.filter(isMcpEnabled);
|
|
2014
|
+
const label = mcpTools.length > 0 ? `MCP stdio enabled (${mcpTools.length} tool${mcpTools.length === 1 ? "" : "s"})` : "MCP stdio enabled (no tools opted in)";
|
|
2015
|
+
log(`${dim}${label}${reset}`);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
function logReload(tools, stdio, log) {
|
|
2019
|
+
log(`
|
|
2020
|
+
Reloaded ${tools.length} tool${tools.length === 1 ? "" : "s"}`);
|
|
2021
|
+
printToolList(tools, log);
|
|
2022
|
+
if (stdio) {
|
|
2023
|
+
const mcpTools = tools.filter(isMcpEnabled);
|
|
2024
|
+
const label = mcpTools.length > 0 ? `MCP stdio enabled (${mcpTools.length} tool${mcpTools.length === 1 ? "" : "s"})` : "MCP stdio enabled (no tools opted in)";
|
|
2025
|
+
log(`${dim}${label}${reset}`);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
function printToolList(tools, log) {
|
|
2029
|
+
tools.forEach((tool) => {
|
|
2030
|
+
const name = tool.metadata?.name ?? tool.filename;
|
|
2031
|
+
const methods = tool.httpHandlers.map((handler) => handler.method).join(", ");
|
|
2032
|
+
const tags = [];
|
|
2033
|
+
if (tool.mcpConfig?.enabled) {
|
|
2034
|
+
tags.push(`${dim}[mcp]${reset}`);
|
|
2035
|
+
}
|
|
2036
|
+
if (tool.payment || tool.metadata && tool.metadata.payment) {
|
|
2037
|
+
tags.push(`${dim}[payments]${reset}`);
|
|
2038
|
+
}
|
|
2039
|
+
const tagSuffix = tags.length ? ` ${tags.join(" ")}` : "";
|
|
2040
|
+
log(` \u2022 ${name} \u2014 ${methods}${tagSuffix}`);
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
async function handleRequest(params) {
|
|
2044
|
+
const { req, res, port, routes } = params;
|
|
2045
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
2046
|
+
res.setHeader(
|
|
2047
|
+
"Access-Control-Allow-Methods",
|
|
2048
|
+
HTTP_METHODS.join(", ") + ", OPTIONS"
|
|
2049
|
+
);
|
|
2050
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
2051
|
+
if (req.method === "OPTIONS") {
|
|
2052
|
+
res.writeHead(200);
|
|
2053
|
+
res.end();
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
const method = (req.method || "GET").toUpperCase();
|
|
2057
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
2058
|
+
const toolName = url.pathname.slice(1) || "index";
|
|
2059
|
+
const route = findRoute(toolName, method, routes);
|
|
2060
|
+
if (!route) {
|
|
2061
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2062
|
+
res.end(
|
|
2063
|
+
JSON.stringify({
|
|
2064
|
+
error: `Tool not found: ${method} /${toolName}`,
|
|
2065
|
+
availableTools: routes.map((r) => `${r.method} /${routeName(r.tool)}`)
|
|
2066
|
+
})
|
|
2067
|
+
);
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
const body = await readRequestBody(req);
|
|
2071
|
+
const request = createWebRequest({ req, url, body });
|
|
2072
|
+
let response;
|
|
2073
|
+
try {
|
|
2074
|
+
response = await route.handler(request);
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
if (error instanceof PaymentRequiredError) {
|
|
2077
|
+
response = error.response;
|
|
2078
|
+
} else {
|
|
2079
|
+
throw error;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
const headers = {};
|
|
2083
|
+
response.headers.forEach((value, key) => {
|
|
2084
|
+
headers[key] = value;
|
|
2085
|
+
});
|
|
2086
|
+
res.writeHead(response.status, headers);
|
|
2087
|
+
if (method === "HEAD") {
|
|
2088
|
+
res.end();
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
2092
|
+
res.end(Buffer.from(arrayBuffer));
|
|
2093
|
+
}
|
|
2094
|
+
function findRoute(toolName, method, routes) {
|
|
2095
|
+
const direct = routes.find(
|
|
2096
|
+
(route) => routeName(route.tool) === toolName && route.method === method
|
|
2097
|
+
);
|
|
2098
|
+
if (direct) {
|
|
2099
|
+
return direct;
|
|
2100
|
+
}
|
|
2101
|
+
if (method === "HEAD") {
|
|
2102
|
+
return routes.find(
|
|
2103
|
+
(route) => routeName(route.tool) === toolName && route.method === "GET"
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
return void 0;
|
|
2107
|
+
}
|
|
2108
|
+
function routeName(tool) {
|
|
2109
|
+
return tool.metadata?.name ?? tool.filename;
|
|
2110
|
+
}
|
|
2111
|
+
async function readRequestBody(req) {
|
|
2112
|
+
const chunks = [];
|
|
2113
|
+
for await (const chunk of req) {
|
|
2114
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
2115
|
+
}
|
|
2116
|
+
return Buffer.concat(chunks);
|
|
2117
|
+
}
|
|
2118
|
+
function createWebRequest(params) {
|
|
2119
|
+
const { req, url, body } = params;
|
|
2120
|
+
const headers = new Headers();
|
|
2121
|
+
Object.entries(req.headers).forEach(([key, value]) => {
|
|
2122
|
+
if (value === void 0) {
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
if (Array.isArray(value)) {
|
|
2126
|
+
value.forEach((entry) => headers.append(key, entry));
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
headers.set(key, value);
|
|
2130
|
+
});
|
|
2131
|
+
const method = (req.method || "GET").toUpperCase();
|
|
2132
|
+
const init = {
|
|
2133
|
+
method,
|
|
2134
|
+
headers
|
|
2135
|
+
};
|
|
2136
|
+
if (body.length > 0 && method !== "GET" && method !== "HEAD") {
|
|
2137
|
+
init.body = body.toString();
|
|
2138
|
+
}
|
|
2139
|
+
return new Request(url, init);
|
|
2140
|
+
}
|
|
2141
|
+
function toHttpHandlerMap2(handlers) {
|
|
2142
|
+
return handlers.reduce(
|
|
2143
|
+
(acc, handler) => {
|
|
2144
|
+
acc[handler.method.toUpperCase()] = handler.handler;
|
|
2145
|
+
return acc;
|
|
2146
|
+
},
|
|
2147
|
+
{}
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
function isMcpEnabled(tool) {
|
|
2151
|
+
return Boolean(tool.mcpConfig?.enabled);
|
|
2152
|
+
}
|
|
2153
|
+
async function generateMetadataCommand(options) {
|
|
2154
|
+
const startTimestamp = timestamp2();
|
|
2155
|
+
console.log(`[${startTimestamp}] Generating OpenTool metadata...`);
|
|
2156
|
+
try {
|
|
2157
|
+
const result = await generateMetadata(options);
|
|
2158
|
+
const endTimestamp = timestamp2();
|
|
2159
|
+
console.log(`[${endTimestamp}] Metadata generation completed successfully!`);
|
|
2160
|
+
console.log(`Output file: ${result.outputPath}`);
|
|
2161
|
+
console.log(`Spec version: ${result.metadata.metadataSpecVersion}`);
|
|
2162
|
+
console.log(`Tools included: ${result.tools.length}`);
|
|
2163
|
+
if (result.defaultsApplied.length > 0) {
|
|
2164
|
+
console.log("Applied defaults:");
|
|
2165
|
+
for (const entry of result.defaultsApplied) {
|
|
2166
|
+
console.log(` \u2022 ${entry}`);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
} catch (error) {
|
|
2170
|
+
const endTimestamp = timestamp2();
|
|
2171
|
+
console.error(`[${endTimestamp}] Metadata generation failed:`, error);
|
|
2172
|
+
process.exit(1);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
async function generateMetadata(options) {
|
|
2176
|
+
const toolsDir = path5.resolve(options.input);
|
|
2177
|
+
if (!fs4.existsSync(toolsDir)) {
|
|
2178
|
+
throw new Error(`Tools directory not found: ${toolsDir}`);
|
|
2179
|
+
}
|
|
2180
|
+
const projectRoot = path5.dirname(toolsDir);
|
|
2181
|
+
const tools = await loadAndValidateTools(toolsDir, { projectRoot });
|
|
2182
|
+
const { metadata, defaultsApplied } = await buildMetadataArtifact({
|
|
2183
|
+
projectRoot,
|
|
2184
|
+
tools
|
|
2185
|
+
});
|
|
2186
|
+
const outputPath = options.output ? path5.resolve(options.output) : path5.join(projectRoot, "metadata.json");
|
|
2187
|
+
fs4.writeFileSync(outputPath, JSON.stringify(metadata, null, 2));
|
|
2188
|
+
return {
|
|
2189
|
+
metadata,
|
|
2190
|
+
defaultsApplied,
|
|
2191
|
+
tools,
|
|
2192
|
+
outputPath
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
function timestamp2() {
|
|
2196
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// src/cli/index.ts
|
|
2200
|
+
program.name("opentool").description("OpenTool CLI for building and developing serverless MCP tools").version("1.0.0");
|
|
2201
|
+
program.command("dev").description("Start HTTP dev server (optional MCP stdio)").option("-i, --input <dir>", "Input directory containing tools", "tools").option("-p, --port <port>", "Port to listen on", "7000").option("--stdio", "Expose MCP stdio transport", false).option("--no-watch", "Disable file watching").action((cmdOptions) => {
|
|
2202
|
+
devCommand({
|
|
2203
|
+
input: cmdOptions.input,
|
|
2204
|
+
port: Number(cmdOptions.port ?? 7e3),
|
|
2205
|
+
watch: cmdOptions.watch,
|
|
2206
|
+
stdio: cmdOptions.stdio
|
|
2207
|
+
});
|
|
2208
|
+
});
|
|
2209
|
+
program.command("build").description("Build tools for deployment").option("-i, --input <dir>", "Input directory containing tools", "tools").option("-o, --output <dir>", "Output directory for built tools", "dist").option("--name <name>", "Server name", "opentool-server").option("--version <version>", "Server version", "1.0.0").action(buildCommand);
|
|
2210
|
+
program.command("validate").description("Validate metadata for registry submission").option("-i, --input <dir>", "Input directory containing tools", "tools").action(validateCommand);
|
|
2211
|
+
program.command("validate-full").description("Full validation of tools and metadata").option("-i, --input <dir>", "Input directory containing tools", "tools").action(validateFullCommand);
|
|
2212
|
+
program.command("metadata").description("Generate OpenTool metadata JSON without building").option("-i, --input <dir>", "Input directory containing tools", "tools").option(
|
|
2213
|
+
"-o, --output <file>",
|
|
2214
|
+
"Output file path for metadata.json",
|
|
2215
|
+
"metadata.json"
|
|
2216
|
+
).option("--name <name>", "Server name", "opentool-server").option("--version <version>", "Server version", "1.0.0").action(generateMetadataCommand);
|
|
2217
|
+
program.parse();
|
|
2218
|
+
|
|
2219
|
+
export { buildCommand, buildProject, devCommand, generateMetadata, generateMetadataCommand, loadAndValidateTools, validateCommand, validateFullCommand };
|
|
2220
|
+
//# sourceMappingURL=index.js.map
|
|
70
2221
|
//# sourceMappingURL=index.js.map
|