x402-bazaar 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/bin/cli.js +62 -0
- package/package.json +49 -0
- package/src/commands/config.js +137 -0
- package/src/commands/init.js +778 -0
- package/src/commands/status.js +63 -0
- package/src/detectors/environment.js +121 -0
- package/src/generators/env-file.js +32 -0
- package/src/generators/mcp-config.js +95 -0
- package/src/utils/logger.js +58 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync } from 'fs';
|
|
5
|
+
import { join, dirname, resolve } from 'path';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { log } from '../utils/logger.js';
|
|
9
|
+
import { detectEnvironment, getOsLabel, getDefaultInstallDir } from '../detectors/environment.js';
|
|
10
|
+
import { generateMcpConfig } from '../generators/mcp-config.js';
|
|
11
|
+
import { generateEnvContent } from '../generators/env-file.js';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// Possible locations for the MCP server source
|
|
17
|
+
const MCP_SERVER_CANDIDATES = [
|
|
18
|
+
join(__dirname, '..', '..', '..', 'x402-bazaar', 'mcp-server.mjs'), // Monorepo layout
|
|
19
|
+
join(__dirname, '..', '..', 'mcp-server.mjs'), // Bundled with CLI
|
|
20
|
+
join(process.cwd(), 'mcp-server.mjs'), // Current directory
|
|
21
|
+
join(process.cwd(), 'x402-bazaar', 'mcp-server.mjs'), // Subdirectory
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function findMcpServerSource() {
|
|
25
|
+
for (const candidate of MCP_SERVER_CANDIDATES) {
|
|
26
|
+
if (existsSync(candidate)) return candidate;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function initCommand(options) {
|
|
32
|
+
// Node version check
|
|
33
|
+
const nodeVersion = parseInt(process.versions.node.split('.')[0], 10);
|
|
34
|
+
if (nodeVersion < 18) {
|
|
35
|
+
log.error(`Node.js >= 18 is required (you have ${process.versions.node}).`);
|
|
36
|
+
log.dim(' Download the latest version at https://nodejs.org');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
log.banner();
|
|
41
|
+
|
|
42
|
+
const osLabel = getOsLabel();
|
|
43
|
+
log.info(`OS: ${chalk.bold(osLabel)} | Node: ${chalk.bold(process.versions.node)}`);
|
|
44
|
+
log.separator();
|
|
45
|
+
console.log('');
|
|
46
|
+
|
|
47
|
+
// ─── Step 1: Detect Environment ─────────────────────────────────────
|
|
48
|
+
log.step(1, 'Detecting AI client environment...');
|
|
49
|
+
console.log('');
|
|
50
|
+
|
|
51
|
+
const environments = detectEnvironment();
|
|
52
|
+
const detected = environments.filter(e => e.detected);
|
|
53
|
+
|
|
54
|
+
let targetEnv;
|
|
55
|
+
|
|
56
|
+
if (options.env) {
|
|
57
|
+
targetEnv = environments.find(e => e.name === options.env) || {
|
|
58
|
+
name: options.env,
|
|
59
|
+
label: options.env,
|
|
60
|
+
configPath: null,
|
|
61
|
+
detected: false,
|
|
62
|
+
};
|
|
63
|
+
log.info(`Using: ${chalk.bold(targetEnv.label)}`);
|
|
64
|
+
} else if (detected.length === 1) {
|
|
65
|
+
targetEnv = detected[0];
|
|
66
|
+
log.success(`Auto-detected: ${chalk.bold(targetEnv.label)}`);
|
|
67
|
+
if (targetEnv.configPath) {
|
|
68
|
+
log.dim(` Config: ${targetEnv.configPath}`);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
if (detected.length > 1) {
|
|
72
|
+
log.info(`Found ${detected.length} AI clients.`);
|
|
73
|
+
} else {
|
|
74
|
+
log.warn('No AI client detected automatically.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const choices = [
|
|
78
|
+
...environments.map(e => ({
|
|
79
|
+
name: `${e.label}${e.detected ? chalk.hex('#34D399')(' (detected)') : ''}`,
|
|
80
|
+
value: e.name,
|
|
81
|
+
})),
|
|
82
|
+
new inquirer.Separator(),
|
|
83
|
+
{ name: 'Generic (I\'ll configure manually)', value: 'generic' },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const { env } = await inquirer.prompt([{
|
|
87
|
+
type: 'list',
|
|
88
|
+
name: 'env',
|
|
89
|
+
message: 'Which AI client are you using?',
|
|
90
|
+
choices,
|
|
91
|
+
default: detected.length > 0 ? detected[0].name : 'claude-desktop',
|
|
92
|
+
}]);
|
|
93
|
+
|
|
94
|
+
targetEnv = environments.find(e => e.name === env) || {
|
|
95
|
+
name: env, label: env, configPath: null, detected: false,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log('');
|
|
100
|
+
|
|
101
|
+
// ─── Step 2: Install MCP Server ─────────────────────────────────────
|
|
102
|
+
log.step(2, 'Setting up MCP server files...');
|
|
103
|
+
console.log('');
|
|
104
|
+
|
|
105
|
+
const installDir = getDefaultInstallDir();
|
|
106
|
+
log.info(`Install directory: ${chalk.dim(installDir)}`);
|
|
107
|
+
|
|
108
|
+
const spinner = ora('Creating directory and copying files...').start();
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
if (!existsSync(installDir)) {
|
|
112
|
+
mkdirSync(installDir, { recursive: true });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const mcpServerDest = join(installDir, 'mcp-server.mjs');
|
|
116
|
+
const mcpSource = findMcpServerSource();
|
|
117
|
+
|
|
118
|
+
if (mcpSource) {
|
|
119
|
+
copyFileSync(mcpSource, mcpServerDest);
|
|
120
|
+
spinner.text = 'Copied mcp-server.mjs from local project...';
|
|
121
|
+
} else {
|
|
122
|
+
spinner.text = 'Generating MCP server file...';
|
|
123
|
+
writeFileSync(mcpServerDest, generateMcpServerFile());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Create package.json for the MCP server runtime
|
|
127
|
+
const pkgJsonPath = join(installDir, 'package.json');
|
|
128
|
+
if (!existsSync(pkgJsonPath)) {
|
|
129
|
+
writeFileSync(pkgJsonPath, JSON.stringify({
|
|
130
|
+
name: 'x402-bazaar-mcp',
|
|
131
|
+
version: '1.0.0',
|
|
132
|
+
type: 'module',
|
|
133
|
+
private: true,
|
|
134
|
+
dependencies: {
|
|
135
|
+
'@coinbase/coinbase-sdk': '^0.25.0',
|
|
136
|
+
'@modelcontextprotocol/sdk': '^1.26.0',
|
|
137
|
+
'dotenv': '^17.2.4',
|
|
138
|
+
'zod': '^4.3.6',
|
|
139
|
+
},
|
|
140
|
+
}, null, 2));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create .gitignore in install dir
|
|
144
|
+
const gitignorePath = join(installDir, '.gitignore');
|
|
145
|
+
if (!existsSync(gitignorePath)) {
|
|
146
|
+
writeFileSync(gitignorePath, 'node_modules/\n.env\n*.seed.json\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
spinner.succeed('MCP server files ready');
|
|
150
|
+
} catch (err) {
|
|
151
|
+
spinner.fail(`Failed to set up files: ${err.message}`);
|
|
152
|
+
log.error('You may need to create the directory manually:');
|
|
153
|
+
log.dim(` mkdir "${installDir}"`);
|
|
154
|
+
log.dim('Then re-run: npx x402-bazaar init');
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// npm install in the install directory
|
|
159
|
+
const spinnerNpm = ora('Installing dependencies (this may take a minute)...').start();
|
|
160
|
+
try {
|
|
161
|
+
execSync('npm install --no-fund --no-audit', {
|
|
162
|
+
cwd: installDir,
|
|
163
|
+
stdio: 'pipe',
|
|
164
|
+
timeout: 180000,
|
|
165
|
+
});
|
|
166
|
+
spinnerNpm.succeed('Dependencies installed');
|
|
167
|
+
} catch (err) {
|
|
168
|
+
spinnerNpm.warn('npm install had issues');
|
|
169
|
+
log.dim(` You can install manually: cd "${installDir}" && npm install`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log('');
|
|
173
|
+
|
|
174
|
+
// ─── Step 3: Wallet Configuration ───────────────────────────────────
|
|
175
|
+
log.step(3, 'Configuring wallet...');
|
|
176
|
+
console.log('');
|
|
177
|
+
|
|
178
|
+
let walletMode = 'readonly';
|
|
179
|
+
let coinbaseApiKey = '';
|
|
180
|
+
let coinbaseApiSecret = '';
|
|
181
|
+
let seedPath = '';
|
|
182
|
+
let maxBudget = '1.00';
|
|
183
|
+
let network = 'mainnet';
|
|
184
|
+
let serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
|
|
185
|
+
|
|
186
|
+
if (options.wallet === false) {
|
|
187
|
+
log.info('Skipping wallet setup (--no-wallet)');
|
|
188
|
+
walletMode = 'readonly';
|
|
189
|
+
} else {
|
|
190
|
+
const { mode } = await inquirer.prompt([{
|
|
191
|
+
type: 'list',
|
|
192
|
+
name: 'mode',
|
|
193
|
+
message: 'How do you want to configure payments?',
|
|
194
|
+
choices: [
|
|
195
|
+
{
|
|
196
|
+
name: `${chalk.bold('I have Coinbase API keys')} — Full access (search, register, pay)`,
|
|
197
|
+
value: 'existing',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: `${chalk.bold('Create a new wallet')} — Guide me through setup`,
|
|
201
|
+
value: 'new',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: `${chalk.bold('Read-only mode')} — Browse marketplace for free (no payments)`,
|
|
205
|
+
value: 'readonly',
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
}]);
|
|
209
|
+
|
|
210
|
+
walletMode = mode;
|
|
211
|
+
|
|
212
|
+
if (mode === 'existing') {
|
|
213
|
+
const walletAnswers = await inquirer.prompt([
|
|
214
|
+
{
|
|
215
|
+
type: 'input',
|
|
216
|
+
name: 'coinbaseApiKey',
|
|
217
|
+
message: 'Coinbase API Key (from portal.cdp.coinbase.com):',
|
|
218
|
+
validate: (v) => v.trim().length > 0 || 'API key is required',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
type: 'password',
|
|
222
|
+
name: 'coinbaseApiSecret',
|
|
223
|
+
message: 'Coinbase API Secret:',
|
|
224
|
+
mask: '*',
|
|
225
|
+
validate: (v) => v.trim().length > 0 || 'API secret is required',
|
|
226
|
+
},
|
|
227
|
+
]);
|
|
228
|
+
coinbaseApiKey = walletAnswers.coinbaseApiKey.trim();
|
|
229
|
+
coinbaseApiSecret = walletAnswers.coinbaseApiSecret.trim();
|
|
230
|
+
|
|
231
|
+
// Check if agent-seed.json exists
|
|
232
|
+
const existingSeed = join(installDir, 'agent-seed.json');
|
|
233
|
+
if (!existsSync(existingSeed)) {
|
|
234
|
+
log.warn('No agent-seed.json found. You will need to create a wallet.');
|
|
235
|
+
log.dim(' Run: cd "' + installDir + '" && node -e "const{Coinbase,Wallet}=require(\'@coinbase/coinbase-sdk\');...');
|
|
236
|
+
log.dim(' Or copy your existing agent-seed.json to: ' + installDir);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (mode === 'new') {
|
|
241
|
+
console.log('');
|
|
242
|
+
log.info('To get Coinbase API keys:');
|
|
243
|
+
console.log('');
|
|
244
|
+
log.dim(' 1. Go to https://portal.cdp.coinbase.com/');
|
|
245
|
+
log.dim(' 2. Create a project (free)');
|
|
246
|
+
log.dim(' 3. Go to "API Keys" and generate a key pair');
|
|
247
|
+
log.dim(' 4. Save both the API Key Name and the Private Key');
|
|
248
|
+
log.dim(' 5. Run npx x402-bazaar init again with your keys');
|
|
249
|
+
console.log('');
|
|
250
|
+
|
|
251
|
+
const { proceed } = await inquirer.prompt([{
|
|
252
|
+
type: 'confirm',
|
|
253
|
+
name: 'proceed',
|
|
254
|
+
message: 'Continue in read-only mode for now?',
|
|
255
|
+
default: true,
|
|
256
|
+
}]);
|
|
257
|
+
|
|
258
|
+
if (!proceed) {
|
|
259
|
+
log.info('Run npx x402-bazaar init when you have your API keys.');
|
|
260
|
+
process.exit(0);
|
|
261
|
+
}
|
|
262
|
+
walletMode = 'readonly';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Network & Budget
|
|
266
|
+
const configAnswers = await inquirer.prompt([
|
|
267
|
+
{
|
|
268
|
+
type: 'list',
|
|
269
|
+
name: 'network',
|
|
270
|
+
message: 'Which network?',
|
|
271
|
+
choices: [
|
|
272
|
+
{ name: 'Base Mainnet (real USDC)', value: 'mainnet' },
|
|
273
|
+
{ name: 'Base Sepolia (testnet, free tokens for testing)', value: 'testnet' },
|
|
274
|
+
],
|
|
275
|
+
default: 'mainnet',
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
type: 'input',
|
|
279
|
+
name: 'maxBudget',
|
|
280
|
+
message: 'Max USDC budget per session (safety limit):',
|
|
281
|
+
default: '1.00',
|
|
282
|
+
validate: (v) => {
|
|
283
|
+
const n = parseFloat(v);
|
|
284
|
+
if (isNaN(n) || n <= 0) return 'Must be a positive number';
|
|
285
|
+
if (n > 100) return 'Maximum is 100 USDC per session';
|
|
286
|
+
return true;
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
]);
|
|
290
|
+
|
|
291
|
+
network = configAnswers.network;
|
|
292
|
+
maxBudget = configAnswers.maxBudget;
|
|
293
|
+
seedPath = join(installDir, 'agent-seed.json');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log('');
|
|
297
|
+
|
|
298
|
+
// ─── Step 4: Generate Config ────────────────────────────────────────
|
|
299
|
+
log.step(4, 'Generating configuration...');
|
|
300
|
+
console.log('');
|
|
301
|
+
|
|
302
|
+
const config = generateMcpConfig({
|
|
303
|
+
environment: targetEnv.name,
|
|
304
|
+
installDir,
|
|
305
|
+
serverUrl,
|
|
306
|
+
maxBudget,
|
|
307
|
+
network,
|
|
308
|
+
coinbaseApiKey,
|
|
309
|
+
coinbaseApiSecret,
|
|
310
|
+
seedPath,
|
|
311
|
+
readOnly: walletMode === 'readonly',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Write .env file in install dir
|
|
315
|
+
if (walletMode !== 'readonly') {
|
|
316
|
+
const envContent = generateEnvContent({
|
|
317
|
+
serverUrl,
|
|
318
|
+
maxBudget,
|
|
319
|
+
network,
|
|
320
|
+
coinbaseApiKey,
|
|
321
|
+
coinbaseApiSecret,
|
|
322
|
+
seedPath,
|
|
323
|
+
});
|
|
324
|
+
const envPath = join(installDir, '.env');
|
|
325
|
+
writeFileSync(envPath, envContent);
|
|
326
|
+
log.success(`.env created at ${chalk.dim(envPath)}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Write or merge config into the AI client config file
|
|
330
|
+
if (targetEnv.configPath) {
|
|
331
|
+
const configWritten = writeConfig(targetEnv, config);
|
|
332
|
+
if (configWritten) {
|
|
333
|
+
log.success(`Config written to ${chalk.dim(targetEnv.configPath)}`);
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
log.info('Generated MCP config — copy this into your client config:');
|
|
337
|
+
console.log('');
|
|
338
|
+
console.log(chalk.dim(JSON.stringify(config, null, 2)));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
console.log('');
|
|
342
|
+
|
|
343
|
+
// ─── Step 5: Verify Connection ──────────────────────────────────────
|
|
344
|
+
log.step(5, 'Verifying connection to x402 Bazaar...');
|
|
345
|
+
console.log('');
|
|
346
|
+
|
|
347
|
+
const spinnerCheck = ora('Connecting to marketplace...').start();
|
|
348
|
+
let serverOnline = false;
|
|
349
|
+
let serviceCount = 0;
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const res = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(10000) });
|
|
353
|
+
const data = await res.json();
|
|
354
|
+
spinnerCheck.succeed(`Connected! Server is online (${data.network})`);
|
|
355
|
+
serverOnline = true;
|
|
356
|
+
} catch (err) {
|
|
357
|
+
spinnerCheck.warn('Could not reach server');
|
|
358
|
+
log.dim(' The server may be sleeping (Render free tier wakes up in ~30s).');
|
|
359
|
+
log.dim(' Run: npx x402-bazaar status');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (serverOnline) {
|
|
363
|
+
try {
|
|
364
|
+
const res = await fetch(serverUrl, { signal: AbortSignal.timeout(10000) });
|
|
365
|
+
const data = await res.json();
|
|
366
|
+
serviceCount = data.total_services || 0;
|
|
367
|
+
log.dim(` Marketplace: ${data.name} — ${serviceCount} services available`);
|
|
368
|
+
} catch {
|
|
369
|
+
// silent
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
console.log('');
|
|
374
|
+
log.separator();
|
|
375
|
+
|
|
376
|
+
// ─── Summary ────────────────────────────────────────────────────────
|
|
377
|
+
console.log('');
|
|
378
|
+
log.success(chalk.bold('Setup complete!'));
|
|
379
|
+
console.log('');
|
|
380
|
+
|
|
381
|
+
const restartMsg = {
|
|
382
|
+
'claude-desktop': 'Restart Claude Desktop to activate the MCP server.',
|
|
383
|
+
'cursor': 'Restart Cursor to activate the MCP server.',
|
|
384
|
+
'claude-code': 'Restart Claude Code to activate the MCP server.',
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const summaryLines = [
|
|
388
|
+
`Environment: ${targetEnv.label}`,
|
|
389
|
+
`Install dir: ${installDir}`,
|
|
390
|
+
`Server: ${serverUrl}`,
|
|
391
|
+
`Network: ${network === 'mainnet' ? 'Base Mainnet' : 'Base Sepolia'}`,
|
|
392
|
+
`Budget limit: ${maxBudget} USDC / session`,
|
|
393
|
+
`Wallet: ${walletMode === 'readonly' ? 'Read-only (no payments)' : 'Configured'}`,
|
|
394
|
+
`Services: ${serviceCount > 0 ? serviceCount + ' available' : 'check with npx x402-bazaar status'}`,
|
|
395
|
+
'',
|
|
396
|
+
restartMsg[targetEnv.name] || 'Configure your AI client with the generated JSON above.',
|
|
397
|
+
'',
|
|
398
|
+
'Then try asking your agent:',
|
|
399
|
+
' "Search for weather APIs on x402 Bazaar"',
|
|
400
|
+
' "List all available services on the marketplace"',
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
log.box('What\'s next?', summaryLines.join('\n'));
|
|
404
|
+
|
|
405
|
+
console.log('');
|
|
406
|
+
log.dim(' Need help? https://x402bazaar.org');
|
|
407
|
+
log.dim(' Dashboard: https://x402-api.onrender.com/dashboard');
|
|
408
|
+
log.dim(' Re-configure: npx x402-bazaar init');
|
|
409
|
+
console.log('');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Write config to the target AI client config file.
|
|
414
|
+
* Merges with existing config if file already exists.
|
|
415
|
+
*/
|
|
416
|
+
function writeConfig(envInfo, newConfig) {
|
|
417
|
+
try {
|
|
418
|
+
const configPath = envInfo.configPath;
|
|
419
|
+
const configDir = dirname(configPath);
|
|
420
|
+
|
|
421
|
+
if (!existsSync(configDir)) {
|
|
422
|
+
mkdirSync(configDir, { recursive: true });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let finalConfig = newConfig;
|
|
426
|
+
|
|
427
|
+
if (existsSync(configPath)) {
|
|
428
|
+
try {
|
|
429
|
+
const existing = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
430
|
+
|
|
431
|
+
if (envInfo.name === 'vscode-continue') {
|
|
432
|
+
if (!existing.mcpServers) existing.mcpServers = [];
|
|
433
|
+
const idx = existing.mcpServers.findIndex(s => s.name === 'x402-bazaar');
|
|
434
|
+
if (idx >= 0) {
|
|
435
|
+
existing.mcpServers[idx] = newConfig.mcpServers[0];
|
|
436
|
+
} else {
|
|
437
|
+
existing.mcpServers.push(newConfig.mcpServers[0]);
|
|
438
|
+
}
|
|
439
|
+
finalConfig = existing;
|
|
440
|
+
} else {
|
|
441
|
+
if (!existing.mcpServers) existing.mcpServers = {};
|
|
442
|
+
existing.mcpServers['x402-bazaar'] = newConfig.mcpServers['x402-bazaar'];
|
|
443
|
+
finalConfig = existing;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
log.info('Merged with existing config file');
|
|
447
|
+
} catch {
|
|
448
|
+
log.warn('Could not parse existing config — creating new file');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
writeFileSync(configPath, JSON.stringify(finalConfig, null, 2));
|
|
453
|
+
return true;
|
|
454
|
+
} catch (err) {
|
|
455
|
+
log.error(`Could not write config: ${err.message}`);
|
|
456
|
+
log.info('Copy this JSON manually into your config file:');
|
|
457
|
+
console.log('');
|
|
458
|
+
console.log(JSON.stringify(newConfig, null, 2));
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Generate a standalone MCP server file.
|
|
465
|
+
* Used as fallback when the local x402-bazaar project is not found.
|
|
466
|
+
*/
|
|
467
|
+
function generateMcpServerFile() {
|
|
468
|
+
return `import 'dotenv/config';
|
|
469
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
470
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
471
|
+
import { z } from 'zod';
|
|
472
|
+
import { createRequire } from 'module';
|
|
473
|
+
|
|
474
|
+
const require = createRequire(import.meta.url);
|
|
475
|
+
const { Coinbase, Wallet } = require('@coinbase/coinbase-sdk');
|
|
476
|
+
|
|
477
|
+
// ─── Config ──────────────────────────────────────────────────────────
|
|
478
|
+
const SERVER_URL = process.env.X402_SERVER_URL || 'https://x402-api.onrender.com';
|
|
479
|
+
const MAX_BUDGET = parseFloat(process.env.MAX_BUDGET_USDC || '1.00');
|
|
480
|
+
const NETWORK = process.env.NETWORK || 'mainnet';
|
|
481
|
+
const explorerBase = NETWORK === 'testnet'
|
|
482
|
+
? 'https://sepolia.basescan.org'
|
|
483
|
+
: 'https://basescan.org';
|
|
484
|
+
const networkLabel = NETWORK === 'testnet' ? 'Base Sepolia' : 'Base Mainnet';
|
|
485
|
+
|
|
486
|
+
// ─── Budget Tracking ─────────────────────────────────────────────────
|
|
487
|
+
let sessionSpending = 0;
|
|
488
|
+
const sessionPayments = [];
|
|
489
|
+
|
|
490
|
+
// ─── Wallet ──────────────────────────────────────────────────────────
|
|
491
|
+
let wallet = null;
|
|
492
|
+
let walletReady = false;
|
|
493
|
+
|
|
494
|
+
async function initWallet() {
|
|
495
|
+
if (walletReady) return;
|
|
496
|
+
|
|
497
|
+
Coinbase.configure({
|
|
498
|
+
apiKeyName: process.env.COINBASE_API_KEY,
|
|
499
|
+
privateKey: process.env.COINBASE_API_SECRET,
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const seedPath = process.env.AGENT_SEED_PATH || 'agent-seed.json';
|
|
503
|
+
const fs = await import('fs');
|
|
504
|
+
const seedData = JSON.parse(fs.readFileSync(seedPath, 'utf-8'));
|
|
505
|
+
const seedWalletId = Object.keys(seedData)[0];
|
|
506
|
+
|
|
507
|
+
wallet = await Wallet.fetch(seedWalletId);
|
|
508
|
+
await wallet.loadSeed(seedPath);
|
|
509
|
+
walletReady = true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ─── x402 Payment Flow ──────────────────────────────────────────────
|
|
513
|
+
async function payAndRequest(url, options = {}) {
|
|
514
|
+
const res = await fetch(url, options);
|
|
515
|
+
const body = await res.json();
|
|
516
|
+
|
|
517
|
+
if (res.status !== 402) {
|
|
518
|
+
return body;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// HTTP 402 — Payment Required
|
|
522
|
+
const details = body.payment_details;
|
|
523
|
+
const cost = parseFloat(details.amount);
|
|
524
|
+
|
|
525
|
+
// Budget check
|
|
526
|
+
if (sessionSpending + cost > MAX_BUDGET) {
|
|
527
|
+
throw new Error(
|
|
528
|
+
\`Budget limit reached. Spent: \${sessionSpending.toFixed(2)} USDC / \${MAX_BUDGET.toFixed(2)} USDC limit. \` +
|
|
529
|
+
\`This call costs \${cost} USDC. Increase MAX_BUDGET_USDC env var to allow more spending.\`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
await initWallet();
|
|
534
|
+
|
|
535
|
+
const transfer = await wallet.createTransfer({
|
|
536
|
+
amount: details.amount,
|
|
537
|
+
assetId: Coinbase.assets.Usdc,
|
|
538
|
+
destination: details.recipient,
|
|
539
|
+
});
|
|
540
|
+
const confirmed = await transfer.wait({ timeoutSeconds: 120 });
|
|
541
|
+
const txHash = confirmed.getTransactionHash();
|
|
542
|
+
|
|
543
|
+
// Track spending
|
|
544
|
+
sessionSpending += cost;
|
|
545
|
+
sessionPayments.push({
|
|
546
|
+
amount: cost,
|
|
547
|
+
txHash,
|
|
548
|
+
timestamp: new Date().toISOString(),
|
|
549
|
+
endpoint: url.replace(SERVER_URL, ''),
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Retry with payment proof
|
|
553
|
+
const retryHeaders = { ...options.headers, 'X-Payment-TxHash': txHash };
|
|
554
|
+
const retryRes = await fetch(url, { ...options, headers: retryHeaders });
|
|
555
|
+
const result = await retryRes.json();
|
|
556
|
+
|
|
557
|
+
// Enrich result with payment info
|
|
558
|
+
result._payment = {
|
|
559
|
+
amount: details.amount,
|
|
560
|
+
currency: 'USDC',
|
|
561
|
+
txHash,
|
|
562
|
+
explorer: \`\${explorerBase}/tx/\${txHash}\`,
|
|
563
|
+
session_spent: sessionSpending.toFixed(2),
|
|
564
|
+
session_remaining: (MAX_BUDGET - sessionSpending).toFixed(2),
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ─── MCP Server ─────────────────────────────────────────────────────
|
|
571
|
+
const server = new McpServer({
|
|
572
|
+
name: 'x402-bazaar',
|
|
573
|
+
version: '1.1.0',
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// --- Tool: discover_marketplace (FREE) ---
|
|
577
|
+
server.tool(
|
|
578
|
+
'discover_marketplace',
|
|
579
|
+
'Discover the x402 Bazaar marketplace. Returns available endpoints, total services, and protocol info. Free — no payment needed.',
|
|
580
|
+
{},
|
|
581
|
+
async () => {
|
|
582
|
+
try {
|
|
583
|
+
const res = await fetch(SERVER_URL);
|
|
584
|
+
const data = await res.json();
|
|
585
|
+
return {
|
|
586
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
587
|
+
};
|
|
588
|
+
} catch (err) {
|
|
589
|
+
return {
|
|
590
|
+
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
591
|
+
isError: true,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
// --- Tool: search_services (0.05 USDC) ---
|
|
598
|
+
server.tool(
|
|
599
|
+
'search_services',
|
|
600
|
+
\`Search for API services on x402 Bazaar by keyword. Costs 0.05 USDC (paid automatically). Budget: \${MAX_BUDGET.toFixed(2)} USDC per session. Check get_budget_status before calling if unsure about remaining budget.\`,
|
|
601
|
+
{ query: z.string().describe('Search keyword (e.g. "weather", "crypto", "ai")') },
|
|
602
|
+
async ({ query }) => {
|
|
603
|
+
try {
|
|
604
|
+
const result = await payAndRequest(
|
|
605
|
+
\`\${SERVER_URL}/search?q=\${encodeURIComponent(query)}\`
|
|
606
|
+
);
|
|
607
|
+
return {
|
|
608
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
609
|
+
};
|
|
610
|
+
} catch (err) {
|
|
611
|
+
return {
|
|
612
|
+
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
613
|
+
isError: true,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
// --- Tool: list_services (0.05 USDC) ---
|
|
620
|
+
server.tool(
|
|
621
|
+
'list_services',
|
|
622
|
+
\`List all API services available on x402 Bazaar. Costs 0.05 USDC (paid automatically). Budget: \${MAX_BUDGET.toFixed(2)} USDC per session.\`,
|
|
623
|
+
{},
|
|
624
|
+
async () => {
|
|
625
|
+
try {
|
|
626
|
+
const result = await payAndRequest(\`\${SERVER_URL}/services\`);
|
|
627
|
+
return {
|
|
628
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
629
|
+
};
|
|
630
|
+
} catch (err) {
|
|
631
|
+
return {
|
|
632
|
+
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
633
|
+
isError: true,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
// --- Tool: find_tool_for_task (0.05 USDC — smart service lookup) ---
|
|
640
|
+
server.tool(
|
|
641
|
+
'find_tool_for_task',
|
|
642
|
+
\`Describe what you need in plain English and get the best matching API service ready to call. Returns the single best match with name, URL, price, and usage instructions. Costs 0.05 USDC. Budget: \${MAX_BUDGET.toFixed(2)} USDC per session.\`,
|
|
643
|
+
{ task: z.string().describe('What you need, in natural language (e.g. "get current weather for a city", "translate text to French")') },
|
|
644
|
+
async ({ task }) => {
|
|
645
|
+
try {
|
|
646
|
+
const stopWords = new Set(['i', 'need', 'want', 'to', 'a', 'an', 'the', 'for', 'of', 'and', 'or', 'in', 'on', 'with', 'that', 'this', 'get', 'find', 'me', 'my', 'some', 'can', 'you', 'do', 'is', 'it', 'be', 'have', 'use', 'please', 'should', 'would', 'could']);
|
|
647
|
+
const keywords = task.toLowerCase()
|
|
648
|
+
.replace(/[^a-z0-9\\s]/g, '')
|
|
649
|
+
.split(/\\s+/)
|
|
650
|
+
.filter(w => w.length > 2 && !stopWords.has(w));
|
|
651
|
+
const query = keywords.slice(0, 3).join(' ') || task.slice(0, 30);
|
|
652
|
+
|
|
653
|
+
const result = await payAndRequest(
|
|
654
|
+
\`\${SERVER_URL}/search?q=\${encodeURIComponent(query)}\`
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
const services = result.data || result.services || [];
|
|
658
|
+
if (services.length === 0) {
|
|
659
|
+
return {
|
|
660
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
661
|
+
found: false,
|
|
662
|
+
query_used: query,
|
|
663
|
+
message: \`No services found matching "\${task}". Try rephrasing or use search_services with different keywords.\`,
|
|
664
|
+
_payment: result._payment,
|
|
665
|
+
}, null, 2) }],
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const best = services[0];
|
|
670
|
+
return {
|
|
671
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
672
|
+
found: true,
|
|
673
|
+
query_used: query,
|
|
674
|
+
service: {
|
|
675
|
+
name: best.name,
|
|
676
|
+
description: best.description,
|
|
677
|
+
url: best.url,
|
|
678
|
+
price_usdc: best.price_usdc,
|
|
679
|
+
tags: best.tags,
|
|
680
|
+
},
|
|
681
|
+
action: \`Call this API using call_api("\${best.url}"). \${Number(best.price_usdc) === 0 ? 'This API is free.' : \`This API costs \${best.price_usdc} USDC per call.\`}\`,
|
|
682
|
+
alternatives_count: services.length - 1,
|
|
683
|
+
_payment: result._payment,
|
|
684
|
+
}, null, 2) }],
|
|
685
|
+
};
|
|
686
|
+
} catch (err) {
|
|
687
|
+
return {
|
|
688
|
+
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
689
|
+
isError: true,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
// --- Tool: call_api (FREE — calls external APIs) ---
|
|
696
|
+
server.tool(
|
|
697
|
+
'call_api',
|
|
698
|
+
'Call an external API URL and return the response. Use this to fetch real data from service URLs discovered on the marketplace. Free — no marketplace payment needed.',
|
|
699
|
+
{ url: z.string().url().describe('The full API URL to call') },
|
|
700
|
+
async ({ url }) => {
|
|
701
|
+
try {
|
|
702
|
+
const res = await fetch(url);
|
|
703
|
+
const text = await res.text();
|
|
704
|
+
let parsed;
|
|
705
|
+
try {
|
|
706
|
+
parsed = JSON.parse(text);
|
|
707
|
+
} catch {
|
|
708
|
+
parsed = { response: text.slice(0, 5000) };
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }],
|
|
712
|
+
};
|
|
713
|
+
} catch (err) {
|
|
714
|
+
return {
|
|
715
|
+
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
716
|
+
isError: true,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
// --- Tool: get_wallet_balance (FREE) ---
|
|
723
|
+
server.tool(
|
|
724
|
+
'get_wallet_balance',
|
|
725
|
+
'Check the USDC balance of the agent wallet on-chain. Free.',
|
|
726
|
+
{},
|
|
727
|
+
async () => {
|
|
728
|
+
try {
|
|
729
|
+
await initWallet();
|
|
730
|
+
const balance = await wallet.getBalance(Coinbase.assets.Usdc);
|
|
731
|
+
const address = (await wallet.getDefaultAddress()).getId();
|
|
732
|
+
return {
|
|
733
|
+
content: [{
|
|
734
|
+
type: 'text',
|
|
735
|
+
text: JSON.stringify({
|
|
736
|
+
address,
|
|
737
|
+
balance_usdc: balance.toString(),
|
|
738
|
+
network: networkLabel,
|
|
739
|
+
explorer: \`\${explorerBase}/address/\${address}\`,
|
|
740
|
+
}, null, 2),
|
|
741
|
+
}],
|
|
742
|
+
};
|
|
743
|
+
} catch (err) {
|
|
744
|
+
return {
|
|
745
|
+
content: [{ type: 'text', text: \`Error: \${err.message}\` }],
|
|
746
|
+
isError: true,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
// --- Tool: get_budget_status (FREE) ---
|
|
753
|
+
server.tool(
|
|
754
|
+
'get_budget_status',
|
|
755
|
+
'Check the session spending budget. Shows how much USDC has been spent, remaining budget, and a list of all payments made this session. Free — call this before paid requests to verify budget.',
|
|
756
|
+
{},
|
|
757
|
+
async () => {
|
|
758
|
+
return {
|
|
759
|
+
content: [{
|
|
760
|
+
type: 'text',
|
|
761
|
+
text: JSON.stringify({
|
|
762
|
+
budget_limit: MAX_BUDGET.toFixed(2) + ' USDC',
|
|
763
|
+
spent: sessionSpending.toFixed(2) + ' USDC',
|
|
764
|
+
remaining: (MAX_BUDGET - sessionSpending).toFixed(2) + ' USDC',
|
|
765
|
+
payments_count: sessionPayments.length,
|
|
766
|
+
payments: sessionPayments,
|
|
767
|
+
network: networkLabel,
|
|
768
|
+
}, null, 2),
|
|
769
|
+
}],
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
// ─── Start ──────────────────────────────────────────────────────────
|
|
775
|
+
const transport = new StdioServerTransport();
|
|
776
|
+
await server.connect(transport);
|
|
777
|
+
`;
|
|
778
|
+
}
|