@obolos_tech/mcp-server 0.2.6 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp.d.ts +61 -0
- package/dist/acp.js +402 -0
- package/dist/acp.js.map +1 -0
- package/dist/index.js +1146 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +30 -0
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -16,8 +16,16 @@ import { join } from 'path';
|
|
|
16
16
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
17
17
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
18
18
|
import { z } from 'zod';
|
|
19
|
+
import { createWalletClient, http } from 'viem';
|
|
20
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
21
|
+
import { base } from 'viem/chains';
|
|
19
22
|
import { MarketplaceClient } from './marketplace.js';
|
|
20
23
|
import { PaymentSigner } from './payment.js';
|
|
24
|
+
import { ACPClient } from './acp.js';
|
|
25
|
+
import { ANP_TYPES, getANPDomain, computeContentHash, usdToUsdc, hashListingIntent as hashListingStruct, hashBidIntent as hashBidStruct } from '@obolos_tech/anp-sdk';
|
|
26
|
+
// ─── ANP (Agent Negotiation Protocol) Helpers ───────────────────────────────
|
|
27
|
+
const ANP_SETTLEMENT_ADDRESS = '0xfEa362Bf569e97B20681289fB4D4a64CEBDFa792';
|
|
28
|
+
const ANP_DOMAIN = getANPDomain(8453, ANP_SETTLEMENT_ADDRESS);
|
|
21
29
|
// Shared config with @obolos_tech/cli — written by `obolos setup`
|
|
22
30
|
function loadConfig() {
|
|
23
31
|
try {
|
|
@@ -34,6 +42,15 @@ const OBOLOS_API_URL = process.env.OBOLOS_API_URL || config.api_url || 'https://
|
|
|
34
42
|
const OBOLOS_PRIVATE_KEY = process.env.OBOLOS_PRIVATE_KEY || config.private_key || '';
|
|
35
43
|
const marketplace = new MarketplaceClient(OBOLOS_API_URL);
|
|
36
44
|
const signer = OBOLOS_PRIVATE_KEY ? new PaymentSigner(OBOLOS_PRIVATE_KEY) : null;
|
|
45
|
+
const acpClient = OBOLOS_PRIVATE_KEY ? new ACPClient(OBOLOS_PRIVATE_KEY) : null;
|
|
46
|
+
// ANP wallet client for EIP-712 signing (reuses the same private key)
|
|
47
|
+
const anpWalletClient = OBOLOS_PRIVATE_KEY
|
|
48
|
+
? (() => {
|
|
49
|
+
const pk = OBOLOS_PRIVATE_KEY.startsWith('0x') ? OBOLOS_PRIVATE_KEY : `0x${OBOLOS_PRIVATE_KEY}`;
|
|
50
|
+
const account = privateKeyToAccount(pk);
|
|
51
|
+
return createWalletClient({ account, chain: base, transport: http() });
|
|
52
|
+
})()
|
|
53
|
+
: null;
|
|
37
54
|
const server = new McpServer({
|
|
38
55
|
name: 'obolos',
|
|
39
56
|
version: '0.1.0',
|
|
@@ -280,6 +297,1134 @@ server.tool('get_balance', 'Check the USDC balance of the configured payment wal
|
|
|
280
297
|
};
|
|
281
298
|
}
|
|
282
299
|
});
|
|
300
|
+
// ─── Tool: create_job ──────────────────────────────────────────────────────
|
|
301
|
+
server.tool('create_job', 'Create a new ERC-8183 Agentic Commerce Protocol job on Obolos. ' +
|
|
302
|
+
'Jobs allow escrowed USDC payments for work — client locks funds, provider does the work, ' +
|
|
303
|
+
'evaluator approves/rejects. Use this for task-based agent commerce.', {
|
|
304
|
+
title: z.string().describe('Short job title'),
|
|
305
|
+
description: z.string().describe('Detailed description of what needs to be done'),
|
|
306
|
+
evaluator_address: z.string().describe('Wallet address of the evaluator who will approve/reject. Use your own address for self-evaluation.'),
|
|
307
|
+
provider_address: z.string().optional().describe('Wallet address of the provider. Leave empty for open jobs anyone can pick up.'),
|
|
308
|
+
budget: z.string().optional().describe('Budget in USDC (e.g. "5.00")'),
|
|
309
|
+
expired_at: z.string().optional().describe('Expiry as ISO date or relative (e.g. "2026-04-01T00:00:00Z" or "24h")'),
|
|
310
|
+
hook_address: z.string().optional().describe('Optional hook contract address for custom logic'),
|
|
311
|
+
}, async ({ title, description, evaluator_address, provider_address, budget, expired_at, hook_address }) => {
|
|
312
|
+
const walletAddress = signer?.address;
|
|
313
|
+
if (!walletAddress) {
|
|
314
|
+
return {
|
|
315
|
+
content: [{ type: 'text', text: 'No wallet configured. Run `npx @obolos_tech/cli setup` or set OBOLOS_PRIVATE_KEY.' }],
|
|
316
|
+
isError: true,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
// Create job on-chain first if ACP client is available
|
|
321
|
+
let chainJobId = null;
|
|
322
|
+
let chainTxHash = null;
|
|
323
|
+
if (acpClient) {
|
|
324
|
+
// Parse expiry to unix timestamp (default: 7 days from now)
|
|
325
|
+
let expiredAt;
|
|
326
|
+
if (expired_at) {
|
|
327
|
+
const d = new Date(expired_at);
|
|
328
|
+
if (isNaN(d.getTime())) {
|
|
329
|
+
// Try relative parsing (e.g. "24h", "7d")
|
|
330
|
+
const match = expired_at.match(/^(\d+)\s*(h|d|m)$/i);
|
|
331
|
+
if (match) {
|
|
332
|
+
const num = parseInt(match[1], 10);
|
|
333
|
+
const unit = match[2].toLowerCase();
|
|
334
|
+
const ms = unit === 'h' ? num * 3600000 : unit === 'd' ? num * 86400000 : num * 60000;
|
|
335
|
+
expiredAt = Math.floor((Date.now() + ms) / 1000);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
expiredAt = Math.floor((Date.now() + 7 * 86400000) / 1000);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
expiredAt = Math.floor(d.getTime() / 1000);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
expiredAt = Math.floor((Date.now() + 7 * 86400000) / 1000);
|
|
347
|
+
}
|
|
348
|
+
const result = await acpClient.createJob({
|
|
349
|
+
provider: provider_address || '0x0000000000000000000000000000000000000000',
|
|
350
|
+
evaluator: evaluator_address,
|
|
351
|
+
expiredAt,
|
|
352
|
+
description: description || title,
|
|
353
|
+
hook: hook_address || '0x0000000000000000000000000000000000000000',
|
|
354
|
+
});
|
|
355
|
+
chainJobId = result.jobId.toString();
|
|
356
|
+
chainTxHash = result.txHash;
|
|
357
|
+
}
|
|
358
|
+
// Then create in backend database to keep in sync
|
|
359
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/jobs`, {
|
|
360
|
+
method: 'POST',
|
|
361
|
+
headers: { 'Content-Type': 'application/json', 'x-wallet-address': walletAddress },
|
|
362
|
+
body: JSON.stringify({
|
|
363
|
+
title,
|
|
364
|
+
description,
|
|
365
|
+
evaluator_address,
|
|
366
|
+
provider_address,
|
|
367
|
+
budget,
|
|
368
|
+
expired_at,
|
|
369
|
+
hook_address,
|
|
370
|
+
chain_job_id: chainJobId,
|
|
371
|
+
chain_tx_hash: chainTxHash,
|
|
372
|
+
}),
|
|
373
|
+
});
|
|
374
|
+
const data = await resp.json();
|
|
375
|
+
if (!resp.ok)
|
|
376
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
377
|
+
return {
|
|
378
|
+
content: [{
|
|
379
|
+
type: 'text',
|
|
380
|
+
text: JSON.stringify({
|
|
381
|
+
message: 'Job created successfully',
|
|
382
|
+
job: data,
|
|
383
|
+
on_chain: chainJobId
|
|
384
|
+
? { chain_job_id: chainJobId, tx_hash: chainTxHash, contract: '0xaF3148696242F7Fb74893DC47690e37950807362' }
|
|
385
|
+
: null,
|
|
386
|
+
next_steps: data.provider_address
|
|
387
|
+
? 'Set a budget with setBudget, then fund the escrow with fund_job.'
|
|
388
|
+
: 'Assign a provider, set a budget, then fund the escrow.',
|
|
389
|
+
}, null, 2),
|
|
390
|
+
}],
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
return {
|
|
395
|
+
content: [{ type: 'text', text: `Failed to create job: ${err.message}` }],
|
|
396
|
+
isError: true,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
// ─── Tool: list_jobs ───────────────────────────────────────────────────────
|
|
401
|
+
server.tool('list_jobs', 'Browse ERC-8183 jobs on the Obolos marketplace. Filter by status, client, or provider address.', {
|
|
402
|
+
status: z.enum(['open', 'funded', 'submitted', 'completed', 'rejected', 'expired']).optional()
|
|
403
|
+
.describe('Filter by job status'),
|
|
404
|
+
client: z.string().optional().describe('Filter by client wallet address'),
|
|
405
|
+
provider: z.string().optional().describe('Filter by provider wallet address'),
|
|
406
|
+
page: z.number().optional().describe('Page number (default: 1)'),
|
|
407
|
+
limit: z.number().optional().describe('Results per page (default: 20)'),
|
|
408
|
+
}, async ({ status, client, provider, page, limit }) => {
|
|
409
|
+
try {
|
|
410
|
+
const params = new URLSearchParams();
|
|
411
|
+
if (status)
|
|
412
|
+
params.set('status', status);
|
|
413
|
+
if (client)
|
|
414
|
+
params.set('client', client);
|
|
415
|
+
if (provider)
|
|
416
|
+
params.set('provider', provider);
|
|
417
|
+
if (page)
|
|
418
|
+
params.set('page', String(page));
|
|
419
|
+
if (limit)
|
|
420
|
+
params.set('limit', String(limit));
|
|
421
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/jobs?${params}`);
|
|
422
|
+
const data = await resp.json();
|
|
423
|
+
if (!resp.ok)
|
|
424
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
425
|
+
const summary = data.jobs.map((j) => ({
|
|
426
|
+
id: j.id,
|
|
427
|
+
title: j.title,
|
|
428
|
+
status: j.status,
|
|
429
|
+
budget: j.budget ? `${j.budget} USDC` : 'not set',
|
|
430
|
+
client: j.client_address,
|
|
431
|
+
provider: j.provider_address || 'open',
|
|
432
|
+
created: j.created_at,
|
|
433
|
+
}));
|
|
434
|
+
return {
|
|
435
|
+
content: [{
|
|
436
|
+
type: 'text',
|
|
437
|
+
text: JSON.stringify({
|
|
438
|
+
total: data.pagination.total,
|
|
439
|
+
showing: summary.length,
|
|
440
|
+
jobs: summary,
|
|
441
|
+
tip: 'Use get_job with a job id for full details.',
|
|
442
|
+
}, null, 2),
|
|
443
|
+
}],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
return {
|
|
448
|
+
content: [{ type: 'text', text: `Failed to list jobs: ${err.message}` }],
|
|
449
|
+
isError: true,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
// ─── Tool: get_job ─────────────────────────────────────────────────────────
|
|
454
|
+
server.tool('get_job', 'Get full details for a specific ERC-8183 job including status, budget, addresses, deliverable, and available actions.', {
|
|
455
|
+
id: z.string().describe('The job ID'),
|
|
456
|
+
}, async ({ id }) => {
|
|
457
|
+
try {
|
|
458
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/jobs/${id}`);
|
|
459
|
+
const job = await resp.json();
|
|
460
|
+
if (!resp.ok)
|
|
461
|
+
throw new Error(job.error || `HTTP ${resp.status}`);
|
|
462
|
+
const walletAddress = signer?.address?.toLowerCase();
|
|
463
|
+
const actions = [];
|
|
464
|
+
const s = job.status;
|
|
465
|
+
const isClient = walletAddress === job.client_address?.toLowerCase();
|
|
466
|
+
const isProvider = walletAddress === job.provider_address?.toLowerCase();
|
|
467
|
+
const isEvaluator = walletAddress === job.evaluator_address?.toLowerCase();
|
|
468
|
+
if (s === 'open' && isClient) {
|
|
469
|
+
if (!job.provider_address)
|
|
470
|
+
actions.push('set provider');
|
|
471
|
+
actions.push('set budget', 'fund', 'reject');
|
|
472
|
+
}
|
|
473
|
+
if (s === 'funded' && isProvider)
|
|
474
|
+
actions.push('submit work');
|
|
475
|
+
if (s === 'submitted' && isEvaluator)
|
|
476
|
+
actions.push('complete', 'reject');
|
|
477
|
+
if ((s === 'funded' || s === 'submitted') && job.expired_at && new Date(job.expired_at) < new Date()) {
|
|
478
|
+
actions.push('claim refund (expired)');
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
content: [{
|
|
482
|
+
type: 'text',
|
|
483
|
+
text: JSON.stringify({
|
|
484
|
+
...job,
|
|
485
|
+
your_role: isClient ? 'client' : isProvider ? 'provider' : isEvaluator ? 'evaluator' : 'none',
|
|
486
|
+
available_actions: actions.length > 0 ? actions : ['none — you have no actions for this job in its current state'],
|
|
487
|
+
state_machine: `Open ${s === 'open' ? '◀' : '→'} Funded ${s === 'funded' ? '◀' : '→'} Submitted ${s === 'submitted' ? '◀' : '→'} Completed/Rejected`,
|
|
488
|
+
}, null, 2),
|
|
489
|
+
}],
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
return {
|
|
494
|
+
content: [{ type: 'text', text: `Failed to get job: ${err.message}` }],
|
|
495
|
+
isError: true,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
// ─── Tool: fund_job ────────────────────────────────────────────────────────
|
|
500
|
+
server.tool('fund_job', 'Fund a job\'s USDC escrow. This locks the budget amount in the ACP smart contract. ' +
|
|
501
|
+
'Only the client can fund. Provider and budget must already be set.', {
|
|
502
|
+
id: z.string().describe('The job ID to fund'),
|
|
503
|
+
expected_budget: z.string().describe('Expected budget amount in USDC (front-run protection)'),
|
|
504
|
+
}, async ({ id, expected_budget }) => {
|
|
505
|
+
const walletAddress = signer?.address;
|
|
506
|
+
if (!walletAddress) {
|
|
507
|
+
return {
|
|
508
|
+
content: [{ type: 'text', text: 'No wallet configured.' }],
|
|
509
|
+
isError: true,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
// Fetch the job to get the chain_job_id
|
|
514
|
+
const jobResp = await fetch(`${OBOLOS_API_URL}/api/jobs/${id}`);
|
|
515
|
+
const jobData = await jobResp.json();
|
|
516
|
+
if (!jobResp.ok)
|
|
517
|
+
throw new Error(jobData.error || `HTTP ${jobResp.status}`);
|
|
518
|
+
const job = jobData.job || jobData;
|
|
519
|
+
const chainJobId = job.chain_job_id;
|
|
520
|
+
let txHash = null;
|
|
521
|
+
if (acpClient && chainJobId) {
|
|
522
|
+
// Fund on-chain: approve USDC + call fund()
|
|
523
|
+
txHash = await acpClient.fundJob(BigInt(chainJobId), expected_budget);
|
|
524
|
+
}
|
|
525
|
+
// Update backend
|
|
526
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/jobs/${id}/fund`, {
|
|
527
|
+
method: 'POST',
|
|
528
|
+
headers: { 'Content-Type': 'application/json', 'x-wallet-address': walletAddress },
|
|
529
|
+
body: JSON.stringify({
|
|
530
|
+
expected_budget,
|
|
531
|
+
tx_hash: txHash || 'pending-onchain',
|
|
532
|
+
chain_job_id: chainJobId || null,
|
|
533
|
+
}),
|
|
534
|
+
});
|
|
535
|
+
const data = await resp.json();
|
|
536
|
+
if (!resp.ok)
|
|
537
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
538
|
+
return {
|
|
539
|
+
content: [{
|
|
540
|
+
type: 'text',
|
|
541
|
+
text: JSON.stringify({
|
|
542
|
+
message: `Job funded with ${expected_budget} USDC escrow`,
|
|
543
|
+
job: data,
|
|
544
|
+
on_chain: txHash
|
|
545
|
+
? { tx_hash: txHash, chain_job_id: chainJobId, contract: '0xaF3148696242F7Fb74893DC47690e37950807362' }
|
|
546
|
+
: { note: 'No chain_job_id found — backend-only funding recorded.' },
|
|
547
|
+
}, null, 2),
|
|
548
|
+
}],
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
return {
|
|
553
|
+
content: [{ type: 'text', text: `Failed to fund job: ${err.message}` }],
|
|
554
|
+
isError: true,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
// ─── Tool: submit_job ──────────────────────────────────────────────────────
|
|
559
|
+
server.tool('submit_job', 'Submit work for a funded ERC-8183 job. Only the assigned provider can submit. ' +
|
|
560
|
+
'The deliverable should be a hash, IPFS CID, or URL referencing the completed work.', {
|
|
561
|
+
id: z.string().describe('The job ID'),
|
|
562
|
+
deliverable: z.string().describe('Hash, IPFS CID, or URL of the completed work'),
|
|
563
|
+
}, async ({ id, deliverable }) => {
|
|
564
|
+
const walletAddress = signer?.address;
|
|
565
|
+
if (!walletAddress) {
|
|
566
|
+
return {
|
|
567
|
+
content: [{ type: 'text', text: 'No wallet configured.' }],
|
|
568
|
+
isError: true,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
// Fetch job to get chain_job_id
|
|
573
|
+
const jobResp = await fetch(`${OBOLOS_API_URL}/api/jobs/${id}`);
|
|
574
|
+
const jobData = await jobResp.json();
|
|
575
|
+
if (!jobResp.ok)
|
|
576
|
+
throw new Error(jobData.error || `HTTP ${jobResp.status}`);
|
|
577
|
+
const job = jobData.job || jobData;
|
|
578
|
+
const chainJobId = job.chain_job_id;
|
|
579
|
+
let txHash = null;
|
|
580
|
+
if (acpClient && chainJobId) {
|
|
581
|
+
txHash = await acpClient.submitJob(BigInt(chainJobId), deliverable);
|
|
582
|
+
}
|
|
583
|
+
// Update backend
|
|
584
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/jobs/${id}/submit`, {
|
|
585
|
+
method: 'POST',
|
|
586
|
+
headers: { 'Content-Type': 'application/json', 'x-wallet-address': walletAddress },
|
|
587
|
+
body: JSON.stringify({ deliverable, tx_hash: txHash }),
|
|
588
|
+
});
|
|
589
|
+
const data = await resp.json();
|
|
590
|
+
if (!resp.ok)
|
|
591
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
592
|
+
return {
|
|
593
|
+
content: [{
|
|
594
|
+
type: 'text',
|
|
595
|
+
text: JSON.stringify({
|
|
596
|
+
message: 'Work submitted successfully. Awaiting evaluator review.',
|
|
597
|
+
job: data,
|
|
598
|
+
on_chain: txHash
|
|
599
|
+
? { tx_hash: txHash, chain_job_id: chainJobId, contract: '0xaF3148696242F7Fb74893DC47690e37950807362' }
|
|
600
|
+
: null,
|
|
601
|
+
}, null, 2),
|
|
602
|
+
}],
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
catch (err) {
|
|
606
|
+
return {
|
|
607
|
+
content: [{ type: 'text', text: `Failed to submit work: ${err.message}` }],
|
|
608
|
+
isError: true,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
// ─── Tool: evaluate_job ────────────────────────────────────────────────────
|
|
613
|
+
server.tool('evaluate_job', 'Evaluate a submitted ERC-8183 job — either complete (approve + release payment) or reject (refund client). ' +
|
|
614
|
+
'Only the designated evaluator can call this after the provider has submitted work.', {
|
|
615
|
+
id: z.string().describe('The job ID'),
|
|
616
|
+
action: z.enum(['complete', 'reject']).describe('"complete" to approve and release payment, "reject" to refund client'),
|
|
617
|
+
reason: z.string().optional().describe('Optional reason/attestation for the evaluation decision'),
|
|
618
|
+
}, async ({ id, action, reason }) => {
|
|
619
|
+
const walletAddress = signer?.address;
|
|
620
|
+
if (!walletAddress) {
|
|
621
|
+
return {
|
|
622
|
+
content: [{ type: 'text', text: 'No wallet configured.' }],
|
|
623
|
+
isError: true,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
// Fetch job to get chain_job_id
|
|
628
|
+
const jobResp = await fetch(`${OBOLOS_API_URL}/api/jobs/${id}`);
|
|
629
|
+
const jobData = await jobResp.json();
|
|
630
|
+
if (!jobResp.ok)
|
|
631
|
+
throw new Error(jobData.error || `HTTP ${jobResp.status}`);
|
|
632
|
+
const job = jobData.job || jobData;
|
|
633
|
+
const chainJobId = job.chain_job_id;
|
|
634
|
+
let txHash = null;
|
|
635
|
+
if (acpClient && chainJobId) {
|
|
636
|
+
if (action === 'complete') {
|
|
637
|
+
txHash = await acpClient.completeJob(BigInt(chainJobId), reason);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
txHash = await acpClient.rejectJob(BigInt(chainJobId), reason);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
// Update backend
|
|
644
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/jobs/${id}/${action}`, {
|
|
645
|
+
method: 'POST',
|
|
646
|
+
headers: { 'Content-Type': 'application/json', 'x-wallet-address': walletAddress },
|
|
647
|
+
body: JSON.stringify({ reason, tx_hash: txHash }),
|
|
648
|
+
});
|
|
649
|
+
const data = await resp.json();
|
|
650
|
+
if (!resp.ok)
|
|
651
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
652
|
+
const msg = action === 'complete'
|
|
653
|
+
? 'Job completed! Payment released to provider.'
|
|
654
|
+
: 'Job rejected. Escrow refunded to client.';
|
|
655
|
+
return {
|
|
656
|
+
content: [{
|
|
657
|
+
type: 'text',
|
|
658
|
+
text: JSON.stringify({
|
|
659
|
+
message: msg,
|
|
660
|
+
job: data,
|
|
661
|
+
on_chain: txHash
|
|
662
|
+
? { tx_hash: txHash, chain_job_id: chainJobId, contract: '0xaF3148696242F7Fb74893DC47690e37950807362' }
|
|
663
|
+
: null,
|
|
664
|
+
}, null, 2),
|
|
665
|
+
}],
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
catch (err) {
|
|
669
|
+
return {
|
|
670
|
+
content: [{ type: 'text', text: `Failed to evaluate job: ${err.message}` }],
|
|
671
|
+
isError: true,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
// ─── Tool: create_listing ─────────────────────────────────────────────────
|
|
676
|
+
server.tool('create_listing', 'Create a job listing for agents to bid on. Other agents can browse and submit competing bids. ' +
|
|
677
|
+
'When you accept a bid, an ACP job is automatically created with the negotiated terms.', {
|
|
678
|
+
title: z.string().describe('Short listing title describing what you need done'),
|
|
679
|
+
description: z.string().describe('Detailed description of the work required'),
|
|
680
|
+
min_budget: z.string().optional().describe('Minimum budget in USDC (e.g. "1.00")'),
|
|
681
|
+
max_budget: z.string().optional().describe('Maximum budget in USDC (e.g. "10.00")'),
|
|
682
|
+
deadline: z.string().optional().describe('Bidding deadline as ISO date or relative (e.g. "7d", "24h")'),
|
|
683
|
+
job_duration: z.number().optional().describe('Expected hours for provider to complete the work'),
|
|
684
|
+
preferred_evaluator: z.string().optional().describe('Preferred evaluator wallet address (0x...)'),
|
|
685
|
+
hook_address: z.string().optional().describe('Optional hook contract address for custom logic'),
|
|
686
|
+
}, async ({ title, description, min_budget, max_budget, deadline, job_duration, preferred_evaluator, hook_address }) => {
|
|
687
|
+
const walletAddress = signer?.address;
|
|
688
|
+
if (!walletAddress) {
|
|
689
|
+
return {
|
|
690
|
+
content: [{ type: 'text', text: 'No wallet configured. Run `npx @obolos_tech/cli setup` or set OBOLOS_PRIVATE_KEY.' }],
|
|
691
|
+
isError: true,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
const payload = { title, description };
|
|
696
|
+
if (min_budget)
|
|
697
|
+
payload.min_budget = min_budget;
|
|
698
|
+
if (max_budget)
|
|
699
|
+
payload.max_budget = max_budget;
|
|
700
|
+
if (deadline)
|
|
701
|
+
payload.deadline = deadline;
|
|
702
|
+
if (job_duration)
|
|
703
|
+
payload.job_duration = job_duration;
|
|
704
|
+
if (preferred_evaluator)
|
|
705
|
+
payload.preferred_evaluator = preferred_evaluator;
|
|
706
|
+
if (hook_address)
|
|
707
|
+
payload.hook_address = hook_address;
|
|
708
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/listings`, {
|
|
709
|
+
method: 'POST',
|
|
710
|
+
headers: { 'Content-Type': 'application/json', 'x-wallet-address': walletAddress },
|
|
711
|
+
body: JSON.stringify(payload),
|
|
712
|
+
});
|
|
713
|
+
const data = await resp.json();
|
|
714
|
+
if (!resp.ok)
|
|
715
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
716
|
+
return {
|
|
717
|
+
content: [{
|
|
718
|
+
type: 'text',
|
|
719
|
+
text: JSON.stringify({
|
|
720
|
+
message: 'Listing created successfully',
|
|
721
|
+
listing: data,
|
|
722
|
+
next_steps: 'Share this listing ID with potential providers. They can submit bids using submit_bid. Use list_listings or get_listing to monitor bids.',
|
|
723
|
+
}, null, 2),
|
|
724
|
+
}],
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
catch (err) {
|
|
728
|
+
return {
|
|
729
|
+
content: [{ type: 'text', text: `Failed to create listing: ${err.message}` }],
|
|
730
|
+
isError: true,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
// ─── Tool: list_listings ──────────────────────────────────────────────────
|
|
735
|
+
server.tool('list_listings', 'Browse open job listings on the Obolos marketplace. Agents can find work opportunities and submit bids.', {
|
|
736
|
+
status: z.enum(['open', 'negotiating', 'accepted', 'cancelled']).optional()
|
|
737
|
+
.describe('Filter by listing status'),
|
|
738
|
+
client: z.string().optional().describe('Filter by client wallet address'),
|
|
739
|
+
page: z.number().optional().describe('Page number (default: 1)'),
|
|
740
|
+
limit: z.number().optional().describe('Results per page (default: 20)'),
|
|
741
|
+
}, async ({ status, client, page, limit }) => {
|
|
742
|
+
try {
|
|
743
|
+
const params = new URLSearchParams();
|
|
744
|
+
if (status)
|
|
745
|
+
params.set('status', status);
|
|
746
|
+
if (client)
|
|
747
|
+
params.set('client', client);
|
|
748
|
+
if (page)
|
|
749
|
+
params.set('page', String(page));
|
|
750
|
+
if (limit)
|
|
751
|
+
params.set('limit', String(limit));
|
|
752
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/listings?${params}`);
|
|
753
|
+
const data = await resp.json();
|
|
754
|
+
if (!resp.ok)
|
|
755
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
756
|
+
const listings = data.listings || data.data || [];
|
|
757
|
+
const summary = listings.map((l) => ({
|
|
758
|
+
id: l.id,
|
|
759
|
+
title: l.title,
|
|
760
|
+
status: l.status,
|
|
761
|
+
budget_range: l.min_budget || l.max_budget
|
|
762
|
+
? `${l.min_budget ? `$${l.min_budget}` : '?'} – ${l.max_budget ? `$${l.max_budget}` : '?'} USDC`
|
|
763
|
+
: 'not set',
|
|
764
|
+
bids: l.bid_count ?? l.bids?.length ?? 0,
|
|
765
|
+
deadline: l.deadline || 'none',
|
|
766
|
+
client: l.client_address,
|
|
767
|
+
created: l.created_at,
|
|
768
|
+
}));
|
|
769
|
+
return {
|
|
770
|
+
content: [{
|
|
771
|
+
type: 'text',
|
|
772
|
+
text: JSON.stringify({
|
|
773
|
+
total: data.pagination?.total || listings.length,
|
|
774
|
+
showing: summary.length,
|
|
775
|
+
listings: summary,
|
|
776
|
+
tip: 'Use get_listing with a listing id to see full details and all bids. Use submit_bid to bid on a listing.',
|
|
777
|
+
}, null, 2),
|
|
778
|
+
}],
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
catch (err) {
|
|
782
|
+
return {
|
|
783
|
+
content: [{ type: 'text', text: `Failed to list listings: ${err.message}` }],
|
|
784
|
+
isError: true,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
// ─── Tool: get_listing ────────────────────────────────────────────────────
|
|
789
|
+
server.tool('get_listing', 'Get full details for a specific listing including all bids from providers. Use this to review bids before accepting one.', {
|
|
790
|
+
id: z.string().describe('The listing ID'),
|
|
791
|
+
}, async ({ id }) => {
|
|
792
|
+
try {
|
|
793
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/listings/${id}`);
|
|
794
|
+
const data = await resp.json();
|
|
795
|
+
if (!resp.ok)
|
|
796
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
797
|
+
const listing = data.listing || data;
|
|
798
|
+
const bids = listing.bids || [];
|
|
799
|
+
const formattedBids = bids.map((b) => ({
|
|
800
|
+
bid_id: b.id,
|
|
801
|
+
provider: b.provider_address,
|
|
802
|
+
price: b.price ? `$${b.price} USDC` : 'not specified',
|
|
803
|
+
delivery_time: b.delivery_time ? `${b.delivery_time}h` : 'not specified',
|
|
804
|
+
message: b.message || '',
|
|
805
|
+
submitted: b.created_at,
|
|
806
|
+
}));
|
|
807
|
+
return {
|
|
808
|
+
content: [{
|
|
809
|
+
type: 'text',
|
|
810
|
+
text: JSON.stringify({
|
|
811
|
+
id: listing.id,
|
|
812
|
+
title: listing.title,
|
|
813
|
+
description: listing.description,
|
|
814
|
+
status: listing.status,
|
|
815
|
+
client: listing.client_address,
|
|
816
|
+
budget_range: {
|
|
817
|
+
min: listing.min_budget ? `$${listing.min_budget} USDC` : 'not set',
|
|
818
|
+
max: listing.max_budget ? `$${listing.max_budget} USDC` : 'not set',
|
|
819
|
+
},
|
|
820
|
+
deadline: listing.deadline || 'none',
|
|
821
|
+
job_duration: listing.job_duration ? `${listing.job_duration}h` : 'not set',
|
|
822
|
+
preferred_evaluator: listing.preferred_evaluator || 'none',
|
|
823
|
+
bids: formattedBids,
|
|
824
|
+
bid_count: formattedBids.length,
|
|
825
|
+
created: listing.created_at,
|
|
826
|
+
tip: listing.status === 'open' && formattedBids.length > 0
|
|
827
|
+
? 'Use accept_bid with the listing_id and bid_id to accept a bid and auto-create an ACP job.'
|
|
828
|
+
: listing.status === 'open'
|
|
829
|
+
? 'No bids yet. Share this listing with providers.'
|
|
830
|
+
: undefined,
|
|
831
|
+
}, null, 2),
|
|
832
|
+
}],
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
catch (err) {
|
|
836
|
+
return {
|
|
837
|
+
content: [{ type: 'text', text: `Failed to get listing: ${err.message}` }],
|
|
838
|
+
isError: true,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
// ─── Tool: submit_bid ─────────────────────────────────────────────────────
|
|
843
|
+
server.tool('submit_bid', 'Submit a bid on a job listing. Propose your price and delivery time to the client. ' +
|
|
844
|
+
'If accepted, an ACP job will be created automatically with you as the provider.', {
|
|
845
|
+
listing_id: z.string().describe('The listing ID to bid on'),
|
|
846
|
+
price: z.string().describe('Your proposed price in USDC (e.g. "5.00")'),
|
|
847
|
+
delivery_time: z.number().optional().describe('Estimated delivery time in hours'),
|
|
848
|
+
message: z.string().optional().describe('Pitch to the client explaining why you should be chosen'),
|
|
849
|
+
proposal_hash: z.string().optional().describe('Optional hash of a detailed proposal document'),
|
|
850
|
+
}, async ({ listing_id, price, delivery_time, message, proposal_hash }) => {
|
|
851
|
+
const walletAddress = signer?.address;
|
|
852
|
+
if (!walletAddress) {
|
|
853
|
+
return {
|
|
854
|
+
content: [{ type: 'text', text: 'No wallet configured. Run `npx @obolos_tech/cli setup` or set OBOLOS_PRIVATE_KEY.' }],
|
|
855
|
+
isError: true,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
try {
|
|
859
|
+
const payload = { price };
|
|
860
|
+
if (delivery_time)
|
|
861
|
+
payload.delivery_time = delivery_time;
|
|
862
|
+
if (message)
|
|
863
|
+
payload.message = message;
|
|
864
|
+
if (proposal_hash)
|
|
865
|
+
payload.proposal_hash = proposal_hash;
|
|
866
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/listings/${listing_id}/bid`, {
|
|
867
|
+
method: 'POST',
|
|
868
|
+
headers: { 'Content-Type': 'application/json', 'x-wallet-address': walletAddress },
|
|
869
|
+
body: JSON.stringify(payload),
|
|
870
|
+
});
|
|
871
|
+
const data = await resp.json();
|
|
872
|
+
if (!resp.ok)
|
|
873
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
874
|
+
return {
|
|
875
|
+
content: [{
|
|
876
|
+
type: 'text',
|
|
877
|
+
text: JSON.stringify({
|
|
878
|
+
message: 'Bid submitted successfully',
|
|
879
|
+
bid: data,
|
|
880
|
+
next_steps: 'The client will review your bid. If accepted, an ACP job will be created with the negotiated terms.',
|
|
881
|
+
}, null, 2),
|
|
882
|
+
}],
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
catch (err) {
|
|
886
|
+
return {
|
|
887
|
+
content: [{ type: 'text', text: `Failed to submit bid: ${err.message}` }],
|
|
888
|
+
isError: true,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
// ─── Tool: accept_bid ─────────────────────────────────────────────────────
|
|
893
|
+
server.tool('accept_bid', 'Accept a bid on your listing. This creates an ACP job automatically with the negotiated price, ' +
|
|
894
|
+
'provider, and evaluator. Only the listing creator can accept bids.', {
|
|
895
|
+
listing_id: z.string().describe('The listing ID'),
|
|
896
|
+
bid_id: z.string().describe('The bid ID to accept'),
|
|
897
|
+
}, async ({ listing_id, bid_id }) => {
|
|
898
|
+
const walletAddress = signer?.address;
|
|
899
|
+
if (!walletAddress) {
|
|
900
|
+
return {
|
|
901
|
+
content: [{ type: 'text', text: 'No wallet configured. Run `npx @obolos_tech/cli setup` or set OBOLOS_PRIVATE_KEY.' }],
|
|
902
|
+
isError: true,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
try {
|
|
906
|
+
// If ACP client available, create on-chain job from the negotiated terms
|
|
907
|
+
let chainJobId = null;
|
|
908
|
+
let chainTxHash = null;
|
|
909
|
+
if (acpClient) {
|
|
910
|
+
// Fetch listing + bid details to get provider, evaluator, etc.
|
|
911
|
+
const listingResp = await fetch(`${OBOLOS_API_URL}/api/listings/${listing_id}`);
|
|
912
|
+
const listingData = await listingResp.json();
|
|
913
|
+
if (!listingResp.ok)
|
|
914
|
+
throw new Error(listingData.error || `HTTP ${listingResp.status}`);
|
|
915
|
+
const listing = listingData.listing || listingData;
|
|
916
|
+
const bids = listing.bids || [];
|
|
917
|
+
const acceptedBid = bids.find((b) => b.id === bid_id);
|
|
918
|
+
if (acceptedBid) {
|
|
919
|
+
const providerAddress = acceptedBid.provider_address || '0x0000000000000000000000000000000000000000';
|
|
920
|
+
const evaluatorAddress = listing.preferred_evaluator || walletAddress;
|
|
921
|
+
// Default expiry: job_duration hours from now, or 7 days
|
|
922
|
+
const durationHours = acceptedBid.delivery_time || listing.job_duration || 168;
|
|
923
|
+
const expiredAt = Math.floor((Date.now() + durationHours * 3600000) / 1000);
|
|
924
|
+
const description = `${listing.title}: ${listing.description || ''}`.slice(0, 500);
|
|
925
|
+
const result = await acpClient.createJob({
|
|
926
|
+
provider: providerAddress,
|
|
927
|
+
evaluator: evaluatorAddress,
|
|
928
|
+
expiredAt,
|
|
929
|
+
description,
|
|
930
|
+
hook: listing.hook_address || '0x0000000000000000000000000000000000000000',
|
|
931
|
+
});
|
|
932
|
+
chainJobId = result.jobId.toString();
|
|
933
|
+
chainTxHash = result.txHash;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
// Accept on backend (will also create backend job record)
|
|
937
|
+
const payload = { bid_id };
|
|
938
|
+
if (chainJobId)
|
|
939
|
+
payload.acp_job_id = chainJobId;
|
|
940
|
+
if (chainTxHash)
|
|
941
|
+
payload.chain_tx_hash = chainTxHash;
|
|
942
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/listings/${listing_id}/accept`, {
|
|
943
|
+
method: 'POST',
|
|
944
|
+
headers: { 'Content-Type': 'application/json', 'x-wallet-address': walletAddress },
|
|
945
|
+
body: JSON.stringify(payload),
|
|
946
|
+
});
|
|
947
|
+
const data = await resp.json();
|
|
948
|
+
if (!resp.ok)
|
|
949
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
950
|
+
return {
|
|
951
|
+
content: [{
|
|
952
|
+
type: 'text',
|
|
953
|
+
text: JSON.stringify({
|
|
954
|
+
message: 'Bid accepted! ACP job created from negotiated terms.',
|
|
955
|
+
listing: data,
|
|
956
|
+
on_chain: chainJobId
|
|
957
|
+
? { chain_job_id: chainJobId, tx_hash: chainTxHash, contract: '0xaF3148696242F7Fb74893DC47690e37950807362' }
|
|
958
|
+
: null,
|
|
959
|
+
next_steps: 'The ACP job has been created. Fund the escrow with fund_job, then the provider can start working.',
|
|
960
|
+
}, null, 2),
|
|
961
|
+
}],
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
catch (err) {
|
|
965
|
+
return {
|
|
966
|
+
content: [{ type: 'text', text: `Failed to accept bid: ${err.message}` }],
|
|
967
|
+
isError: true,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
// ─── ANP Tools (Agent Negotiation Protocol — EIP-712 signed documents) ──────
|
|
972
|
+
server.tool('anp_publish_listing', 'Sign and publish an ANP listing via EIP-712. Creates a cryptographically signed, ' +
|
|
973
|
+
'content-addressed listing document. Other agents can bid on it using anp_publish_bid.', {
|
|
974
|
+
title: z.string().describe('Short listing title'),
|
|
975
|
+
description: z.string().describe('Detailed description of the work required'),
|
|
976
|
+
min_budget: z.number().describe('Minimum budget in USD (e.g. 1.00)'),
|
|
977
|
+
max_budget: z.number().describe('Maximum budget in USD (e.g. 10.00)'),
|
|
978
|
+
deadline_hours: z.number().describe('Hours until bidding deadline'),
|
|
979
|
+
job_duration_hours: z.number().describe('Expected hours for provider to complete the work'),
|
|
980
|
+
evaluator: z.string().optional().describe('Preferred evaluator wallet address (0x...)'),
|
|
981
|
+
}, async ({ title, description, min_budget, max_budget, deadline_hours, job_duration_hours, evaluator }) => {
|
|
982
|
+
if (!anpWalletClient) {
|
|
983
|
+
return {
|
|
984
|
+
content: [{ type: 'text', text: 'No wallet configured. Set OBOLOS_PRIVATE_KEY or run `npx @obolos_tech/cli setup`.' }],
|
|
985
|
+
isError: true,
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
try {
|
|
989
|
+
const minBudget = usdToUsdc(min_budget);
|
|
990
|
+
const maxBudget = usdToUsdc(max_budget);
|
|
991
|
+
const contentHash = await computeContentHash({ title, description });
|
|
992
|
+
const nonce = Math.floor(Math.random() * 2 ** 32);
|
|
993
|
+
const deadline = Math.floor(Date.now() / 1000) + deadline_hours * 3600;
|
|
994
|
+
const jobDuration = job_duration_hours * 3600;
|
|
995
|
+
const preferredEvaluator = (evaluator || '0x0000000000000000000000000000000000000000');
|
|
996
|
+
const message = {
|
|
997
|
+
contentHash,
|
|
998
|
+
minBudget: BigInt(minBudget),
|
|
999
|
+
maxBudget: BigInt(maxBudget),
|
|
1000
|
+
deadline: BigInt(deadline),
|
|
1001
|
+
jobDuration: BigInt(jobDuration),
|
|
1002
|
+
preferredEvaluator,
|
|
1003
|
+
nonce: BigInt(nonce),
|
|
1004
|
+
};
|
|
1005
|
+
const signature = await anpWalletClient.signTypedData({
|
|
1006
|
+
domain: ANP_DOMAIN,
|
|
1007
|
+
types: ANP_TYPES,
|
|
1008
|
+
primaryType: 'ListingIntent',
|
|
1009
|
+
message,
|
|
1010
|
+
});
|
|
1011
|
+
const signerAddress = anpWalletClient.account.address.toLowerCase();
|
|
1012
|
+
const document = {
|
|
1013
|
+
protocol: 'anp/v1',
|
|
1014
|
+
type: 'listing',
|
|
1015
|
+
data: {
|
|
1016
|
+
title,
|
|
1017
|
+
description,
|
|
1018
|
+
minBudget,
|
|
1019
|
+
maxBudget,
|
|
1020
|
+
deadline,
|
|
1021
|
+
jobDuration,
|
|
1022
|
+
preferredEvaluator,
|
|
1023
|
+
nonce,
|
|
1024
|
+
},
|
|
1025
|
+
signer: signerAddress,
|
|
1026
|
+
signature,
|
|
1027
|
+
timestamp: Date.now(),
|
|
1028
|
+
};
|
|
1029
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/anp/publish`, {
|
|
1030
|
+
method: 'POST',
|
|
1031
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1032
|
+
body: JSON.stringify(document),
|
|
1033
|
+
});
|
|
1034
|
+
const data = await resp.json();
|
|
1035
|
+
if (!resp.ok)
|
|
1036
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
1037
|
+
return {
|
|
1038
|
+
content: [{
|
|
1039
|
+
type: 'text',
|
|
1040
|
+
text: JSON.stringify({
|
|
1041
|
+
message: 'ANP listing published successfully',
|
|
1042
|
+
cid: data.cid,
|
|
1043
|
+
signer: signerAddress,
|
|
1044
|
+
budget_range: `$${min_budget} – $${max_budget} USD`,
|
|
1045
|
+
deadline: new Date(deadline * 1000).toISOString(),
|
|
1046
|
+
next_steps: 'Share the CID with potential providers. They can bid using anp_publish_bid.',
|
|
1047
|
+
}, null, 2),
|
|
1048
|
+
}],
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
catch (err) {
|
|
1052
|
+
return {
|
|
1053
|
+
content: [{ type: 'text', text: `Failed to publish ANP listing: ${err.message}` }],
|
|
1054
|
+
isError: true,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
server.tool('anp_publish_bid', 'Sign and publish an ANP bid on an existing listing. Creates a cryptographically signed bid ' +
|
|
1059
|
+
'document that references the listing by CID and struct hash.', {
|
|
1060
|
+
listing_cid: z.string().describe('CID of the listing to bid on'),
|
|
1061
|
+
price: z.number().describe('Your proposed price in USD (e.g. 5.00)'),
|
|
1062
|
+
delivery_hours: z.number().describe('Estimated delivery time in hours'),
|
|
1063
|
+
message: z.string().optional().describe('Short message to the client'),
|
|
1064
|
+
}, async ({ listing_cid, price, delivery_hours, message: bidMessage }) => {
|
|
1065
|
+
if (!anpWalletClient) {
|
|
1066
|
+
return {
|
|
1067
|
+
content: [{ type: 'text', text: 'No wallet configured. Set OBOLOS_PRIVATE_KEY or run `npx @obolos_tech/cli setup`.' }],
|
|
1068
|
+
isError: true,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
// Fetch the listing document
|
|
1073
|
+
const listingResp = await fetch(`${OBOLOS_API_URL}/api/anp/objects/${listing_cid}`);
|
|
1074
|
+
const listingDoc = await listingResp.json();
|
|
1075
|
+
if (!listingResp.ok)
|
|
1076
|
+
throw new Error(listingDoc.error || `HTTP ${listingResp.status}`);
|
|
1077
|
+
const ld = listingDoc.data || listingDoc;
|
|
1078
|
+
// Compute the listing struct hash
|
|
1079
|
+
const listingContentHash = await computeContentHash({ title: ld.title, description: ld.description });
|
|
1080
|
+
const listingHash = hashListingStruct({
|
|
1081
|
+
contentHash: listingContentHash,
|
|
1082
|
+
minBudget: BigInt(ld.minBudget),
|
|
1083
|
+
maxBudget: BigInt(ld.maxBudget),
|
|
1084
|
+
deadline: BigInt(ld.deadline),
|
|
1085
|
+
jobDuration: BigInt(ld.jobDuration),
|
|
1086
|
+
preferredEvaluator: (ld.preferredEvaluator || '0x0000000000000000000000000000000000000000'),
|
|
1087
|
+
nonce: BigInt(ld.nonce),
|
|
1088
|
+
});
|
|
1089
|
+
const priceUsdc = usdToUsdc(price);
|
|
1090
|
+
const deliveryTime = delivery_hours * 3600;
|
|
1091
|
+
const contentHash = await computeContentHash({ message: bidMessage || '', proposalCid: '' });
|
|
1092
|
+
const nonce = Math.floor(Math.random() * 2 ** 32);
|
|
1093
|
+
const bidMsg = {
|
|
1094
|
+
listingHash,
|
|
1095
|
+
contentHash,
|
|
1096
|
+
price: BigInt(priceUsdc),
|
|
1097
|
+
deliveryTime: BigInt(deliveryTime),
|
|
1098
|
+
nonce: BigInt(nonce),
|
|
1099
|
+
};
|
|
1100
|
+
const signature = await anpWalletClient.signTypedData({
|
|
1101
|
+
domain: ANP_DOMAIN,
|
|
1102
|
+
types: ANP_TYPES,
|
|
1103
|
+
primaryType: 'BidIntent',
|
|
1104
|
+
message: bidMsg,
|
|
1105
|
+
});
|
|
1106
|
+
const signerAddress = anpWalletClient.account.address.toLowerCase();
|
|
1107
|
+
const document = {
|
|
1108
|
+
protocol: 'anp/v1',
|
|
1109
|
+
type: 'bid',
|
|
1110
|
+
data: {
|
|
1111
|
+
listingCid: listing_cid,
|
|
1112
|
+
listingHash,
|
|
1113
|
+
price: priceUsdc,
|
|
1114
|
+
deliveryTime,
|
|
1115
|
+
message: bidMessage || '',
|
|
1116
|
+
nonce,
|
|
1117
|
+
},
|
|
1118
|
+
signer: signerAddress,
|
|
1119
|
+
signature,
|
|
1120
|
+
timestamp: Date.now(),
|
|
1121
|
+
};
|
|
1122
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/anp/publish`, {
|
|
1123
|
+
method: 'POST',
|
|
1124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1125
|
+
body: JSON.stringify(document),
|
|
1126
|
+
});
|
|
1127
|
+
const data = await resp.json();
|
|
1128
|
+
if (!resp.ok)
|
|
1129
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
1130
|
+
return {
|
|
1131
|
+
content: [{
|
|
1132
|
+
type: 'text',
|
|
1133
|
+
text: JSON.stringify({
|
|
1134
|
+
message: 'ANP bid published successfully',
|
|
1135
|
+
cid: data.cid,
|
|
1136
|
+
listing_cid,
|
|
1137
|
+
price: `$${price} USD`,
|
|
1138
|
+
delivery: `${delivery_hours}h`,
|
|
1139
|
+
signer: signerAddress,
|
|
1140
|
+
next_steps: 'The listing creator can accept your bid using anp_accept_bid.',
|
|
1141
|
+
}, null, 2),
|
|
1142
|
+
}],
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
catch (err) {
|
|
1146
|
+
return {
|
|
1147
|
+
content: [{ type: 'text', text: `Failed to publish ANP bid: ${err.message}` }],
|
|
1148
|
+
isError: true,
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
server.tool('anp_accept_bid', 'Sign and publish an acceptance of an ANP bid. Creates a cryptographically signed acceptance ' +
|
|
1153
|
+
'document that references both the listing and bid by CID and struct hash.', {
|
|
1154
|
+
listing_cid: z.string().describe('CID of the listing'),
|
|
1155
|
+
bid_cid: z.string().describe('CID of the bid to accept'),
|
|
1156
|
+
}, async ({ listing_cid, bid_cid }) => {
|
|
1157
|
+
if (!anpWalletClient) {
|
|
1158
|
+
return {
|
|
1159
|
+
content: [{ type: 'text', text: 'No wallet configured. Set OBOLOS_PRIVATE_KEY or run `npx @obolos_tech/cli setup`.' }],
|
|
1160
|
+
isError: true,
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
try {
|
|
1164
|
+
// Fetch listing and bid documents
|
|
1165
|
+
const [listingResp, bidResp] = await Promise.all([
|
|
1166
|
+
fetch(`${OBOLOS_API_URL}/api/anp/objects/${listing_cid}`),
|
|
1167
|
+
fetch(`${OBOLOS_API_URL}/api/anp/objects/${bid_cid}`),
|
|
1168
|
+
]);
|
|
1169
|
+
const listingDoc = await listingResp.json();
|
|
1170
|
+
const bidDoc = await bidResp.json();
|
|
1171
|
+
if (!listingResp.ok)
|
|
1172
|
+
throw new Error(listingDoc.error || `HTTP ${listingResp.status}`);
|
|
1173
|
+
if (!bidResp.ok)
|
|
1174
|
+
throw new Error(bidDoc.error || `HTTP ${bidResp.status}`);
|
|
1175
|
+
const ld = listingDoc.data || listingDoc;
|
|
1176
|
+
const bd = bidDoc.data || bidDoc;
|
|
1177
|
+
// Compute listing struct hash
|
|
1178
|
+
const listingContentHash = await computeContentHash({ title: ld.title, description: ld.description });
|
|
1179
|
+
const listingHash = hashListingStruct({
|
|
1180
|
+
contentHash: listingContentHash,
|
|
1181
|
+
minBudget: BigInt(ld.minBudget),
|
|
1182
|
+
maxBudget: BigInt(ld.maxBudget),
|
|
1183
|
+
deadline: BigInt(ld.deadline),
|
|
1184
|
+
jobDuration: BigInt(ld.jobDuration),
|
|
1185
|
+
preferredEvaluator: (ld.preferredEvaluator || '0x0000000000000000000000000000000000000000'),
|
|
1186
|
+
nonce: BigInt(ld.nonce),
|
|
1187
|
+
});
|
|
1188
|
+
// Compute bid struct hash
|
|
1189
|
+
const bidContentHash = await computeContentHash({ message: bd.message || '', proposalCid: bd.proposalCid || '' });
|
|
1190
|
+
const bidHash = hashBidStruct({
|
|
1191
|
+
listingHash: bd.listingHash,
|
|
1192
|
+
contentHash: bidContentHash,
|
|
1193
|
+
price: BigInt(bd.price),
|
|
1194
|
+
deliveryTime: BigInt(bd.deliveryTime),
|
|
1195
|
+
nonce: BigInt(bd.nonce),
|
|
1196
|
+
});
|
|
1197
|
+
const nonce = Math.floor(Math.random() * 2 ** 32);
|
|
1198
|
+
const acceptMsg = {
|
|
1199
|
+
listingHash,
|
|
1200
|
+
bidHash,
|
|
1201
|
+
nonce: BigInt(nonce),
|
|
1202
|
+
};
|
|
1203
|
+
const signature = await anpWalletClient.signTypedData({
|
|
1204
|
+
domain: ANP_DOMAIN,
|
|
1205
|
+
types: ANP_TYPES,
|
|
1206
|
+
primaryType: 'AcceptIntent',
|
|
1207
|
+
message: acceptMsg,
|
|
1208
|
+
});
|
|
1209
|
+
const signerAddress = anpWalletClient.account.address.toLowerCase();
|
|
1210
|
+
const document = {
|
|
1211
|
+
protocol: 'anp/v1',
|
|
1212
|
+
type: 'acceptance',
|
|
1213
|
+
data: {
|
|
1214
|
+
listingCid: listing_cid,
|
|
1215
|
+
bidCid: bid_cid,
|
|
1216
|
+
listingHash,
|
|
1217
|
+
bidHash,
|
|
1218
|
+
nonce,
|
|
1219
|
+
},
|
|
1220
|
+
signer: signerAddress,
|
|
1221
|
+
signature,
|
|
1222
|
+
timestamp: Date.now(),
|
|
1223
|
+
};
|
|
1224
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/anp/publish`, {
|
|
1225
|
+
method: 'POST',
|
|
1226
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1227
|
+
body: JSON.stringify(document),
|
|
1228
|
+
});
|
|
1229
|
+
const data = await resp.json();
|
|
1230
|
+
if (!resp.ok)
|
|
1231
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
1232
|
+
return {
|
|
1233
|
+
content: [{
|
|
1234
|
+
type: 'text',
|
|
1235
|
+
text: JSON.stringify({
|
|
1236
|
+
message: 'ANP bid accepted! Acceptance document published.',
|
|
1237
|
+
cid: data.cid,
|
|
1238
|
+
listing_cid,
|
|
1239
|
+
bid_cid,
|
|
1240
|
+
signer: signerAddress,
|
|
1241
|
+
next_steps: 'The three signed documents (listing, bid, acceptance) can now be submitted to the settlement contract on-chain.',
|
|
1242
|
+
}, null, 2),
|
|
1243
|
+
}],
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
catch (err) {
|
|
1247
|
+
return {
|
|
1248
|
+
content: [{ type: 'text', text: `Failed to publish ANP acceptance: ${err.message}` }],
|
|
1249
|
+
isError: true,
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
server.tool('anp_list_listings', 'Browse ANP listings — cryptographically signed job postings from the Agent Negotiation Protocol.', {
|
|
1254
|
+
status: z.enum(['open', 'negotiating', 'accepted']).optional()
|
|
1255
|
+
.describe('Filter by listing status'),
|
|
1256
|
+
page: z.number().optional().describe('Page number (default: 1)'),
|
|
1257
|
+
}, async ({ status, page }) => {
|
|
1258
|
+
try {
|
|
1259
|
+
const params = new URLSearchParams();
|
|
1260
|
+
if (status)
|
|
1261
|
+
params.set('status', status);
|
|
1262
|
+
if (page)
|
|
1263
|
+
params.set('page', String(page));
|
|
1264
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/anp/listings?${params}`);
|
|
1265
|
+
const data = await resp.json();
|
|
1266
|
+
if (!resp.ok)
|
|
1267
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
1268
|
+
const listings = data.listings || data.data || [];
|
|
1269
|
+
const summary = listings.map((l) => {
|
|
1270
|
+
const ld = l.data || l;
|
|
1271
|
+
return {
|
|
1272
|
+
cid: l.cid || l.id,
|
|
1273
|
+
title: ld.title,
|
|
1274
|
+
status: l.status || 'open',
|
|
1275
|
+
budget_range: ld.minBudget && ld.maxBudget
|
|
1276
|
+
? `$${(Number(ld.minBudget) / 1_000_000).toFixed(2)} – $${(Number(ld.maxBudget) / 1_000_000).toFixed(2)} USDC`
|
|
1277
|
+
: 'not set',
|
|
1278
|
+
deadline: ld.deadline ? new Date(Number(ld.deadline) * 1000).toISOString() : 'none',
|
|
1279
|
+
bids: l.bid_count ?? 0,
|
|
1280
|
+
signer: l.signer || ld.signer,
|
|
1281
|
+
};
|
|
1282
|
+
});
|
|
1283
|
+
return {
|
|
1284
|
+
content: [{
|
|
1285
|
+
type: 'text',
|
|
1286
|
+
text: JSON.stringify({
|
|
1287
|
+
total: data.pagination?.total || listings.length,
|
|
1288
|
+
showing: summary.length,
|
|
1289
|
+
listings: summary,
|
|
1290
|
+
tip: 'Use anp_get_listing with a CID for full details. Use anp_publish_bid to bid on a listing.',
|
|
1291
|
+
}, null, 2),
|
|
1292
|
+
}],
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
catch (err) {
|
|
1296
|
+
return {
|
|
1297
|
+
content: [{ type: 'text', text: `Failed to list ANP listings: ${err.message}` }],
|
|
1298
|
+
isError: true,
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
server.tool('anp_get_listing', 'Get full details for an ANP listing including all bids. Shows the signed document data, ' +
|
|
1303
|
+
'budget range, deadline, and all bid documents from providers.', {
|
|
1304
|
+
cid: z.string().describe('The listing CID (content identifier)'),
|
|
1305
|
+
}, async ({ cid }) => {
|
|
1306
|
+
try {
|
|
1307
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/anp/listings/${cid}`);
|
|
1308
|
+
const data = await resp.json();
|
|
1309
|
+
if (!resp.ok)
|
|
1310
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
1311
|
+
const listing = data.listing || data;
|
|
1312
|
+
const ld = listing.data || listing;
|
|
1313
|
+
const bids = listing.bids || [];
|
|
1314
|
+
const formattedBids = bids.map((b) => {
|
|
1315
|
+
const bd = b.data || b;
|
|
1316
|
+
return {
|
|
1317
|
+
cid: b.cid || b.id,
|
|
1318
|
+
provider: b.signer || bd.signer,
|
|
1319
|
+
price: bd.price ? `$${(Number(bd.price) / 1_000_000).toFixed(2)} USDC` : 'not specified',
|
|
1320
|
+
delivery_time: bd.deliveryTime ? `${Math.round(Number(bd.deliveryTime) / 3600)}h` : 'not specified',
|
|
1321
|
+
message: bd.message || '',
|
|
1322
|
+
timestamp: b.timestamp ? new Date(Number(b.timestamp)).toISOString() : '',
|
|
1323
|
+
};
|
|
1324
|
+
});
|
|
1325
|
+
return {
|
|
1326
|
+
content: [{
|
|
1327
|
+
type: 'text',
|
|
1328
|
+
text: JSON.stringify({
|
|
1329
|
+
cid: listing.cid || cid,
|
|
1330
|
+
title: ld.title,
|
|
1331
|
+
description: ld.description,
|
|
1332
|
+
status: listing.status || 'open',
|
|
1333
|
+
signer: listing.signer,
|
|
1334
|
+
budget_range: {
|
|
1335
|
+
min: ld.minBudget ? `$${(Number(ld.minBudget) / 1_000_000).toFixed(2)} USDC` : 'not set',
|
|
1336
|
+
max: ld.maxBudget ? `$${(Number(ld.maxBudget) / 1_000_000).toFixed(2)} USDC` : 'not set',
|
|
1337
|
+
},
|
|
1338
|
+
deadline: ld.deadline ? new Date(Number(ld.deadline) * 1000).toISOString() : 'none',
|
|
1339
|
+
job_duration: ld.jobDuration ? `${Math.round(Number(ld.jobDuration) / 3600)}h` : 'not set',
|
|
1340
|
+
preferred_evaluator: ld.preferredEvaluator || 'none',
|
|
1341
|
+
signature: listing.signature,
|
|
1342
|
+
bids: formattedBids,
|
|
1343
|
+
bid_count: formattedBids.length,
|
|
1344
|
+
tip: formattedBids.length > 0
|
|
1345
|
+
? 'Use anp_accept_bid with the listing CID and bid CID to accept a bid.'
|
|
1346
|
+
: 'No bids yet. Share the CID with potential providers.',
|
|
1347
|
+
}, null, 2),
|
|
1348
|
+
}],
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
catch (err) {
|
|
1352
|
+
return {
|
|
1353
|
+
content: [{ type: 'text', text: `Failed to get ANP listing: ${err.message}` }],
|
|
1354
|
+
isError: true,
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
server.tool('anp_verify', 'Verify the integrity and signature of an ANP document by its CID. ' +
|
|
1359
|
+
'Checks content hash, EIP-712 signature recovery, and cross-references.', {
|
|
1360
|
+
cid: z.string().describe('The document CID to verify'),
|
|
1361
|
+
}, async ({ cid }) => {
|
|
1362
|
+
try {
|
|
1363
|
+
const resp = await fetch(`${OBOLOS_API_URL}/api/anp/verify/${cid}`);
|
|
1364
|
+
const data = await resp.json();
|
|
1365
|
+
if (!resp.ok)
|
|
1366
|
+
throw new Error(data.error || `HTTP ${resp.status}`);
|
|
1367
|
+
return {
|
|
1368
|
+
content: [{
|
|
1369
|
+
type: 'text',
|
|
1370
|
+
text: JSON.stringify({
|
|
1371
|
+
cid,
|
|
1372
|
+
verification: data,
|
|
1373
|
+
}, null, 2),
|
|
1374
|
+
}],
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
catch (err) {
|
|
1378
|
+
return {
|
|
1379
|
+
content: [{ type: 'text', text: `Failed to verify ANP document: ${err.message}` }],
|
|
1380
|
+
isError: true,
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
// ─── Tool: check_reputation ─────────────────────────────────────────────────
|
|
1385
|
+
server.tool('check_reputation', 'Check trust scores for an agent from RNWY + AgentProof reputation providers. Returns combined score, tier, pass/fail, sybil flags.', {
|
|
1386
|
+
agent_id: z.number().describe('Agent ID (token ID or registry ID)'),
|
|
1387
|
+
chain: z.string().optional().default('base').describe('Chain slug (base, ethereum, etc.)'),
|
|
1388
|
+
address: z.string().optional().describe('Wallet address of the agent'),
|
|
1389
|
+
}, async ({ agent_id, chain, address }) => {
|
|
1390
|
+
try {
|
|
1391
|
+
const url = new URL(`${OBOLOS_API_URL}/api/anp/reputation/${agent_id}`);
|
|
1392
|
+
if (chain)
|
|
1393
|
+
url.searchParams.set('chain', chain);
|
|
1394
|
+
if (address)
|
|
1395
|
+
url.searchParams.set('address', address);
|
|
1396
|
+
const res = await fetch(url.toString());
|
|
1397
|
+
if (!res.ok)
|
|
1398
|
+
throw new Error(`HTTP ${res.status}`);
|
|
1399
|
+
const data = await res.json();
|
|
1400
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
1401
|
+
}
|
|
1402
|
+
catch (e) {
|
|
1403
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
// ─── Tool: compare_reputations ──────────────────────────────────────────────
|
|
1407
|
+
server.tool('compare_reputations', 'Compare trust scores of multiple agents side-by-side. Useful when choosing between bid providers.', {
|
|
1408
|
+
agents: z.string().describe('Comma-separated chain:id pairs, e.g. "base:123,base:456,base:789"'),
|
|
1409
|
+
}, async ({ agents }) => {
|
|
1410
|
+
try {
|
|
1411
|
+
const pairs = agents.split(',').map(p => p.trim());
|
|
1412
|
+
const results = await Promise.all(pairs.map(async (pair) => {
|
|
1413
|
+
const [chain, id] = pair.includes(':') ? pair.split(':') : ['base', pair];
|
|
1414
|
+
const url = `${OBOLOS_API_URL}/api/anp/reputation/${id}?chain=${chain}`;
|
|
1415
|
+
const res = await fetch(url);
|
|
1416
|
+
if (!res.ok)
|
|
1417
|
+
return { agentId: id, chain, error: `HTTP ${res.status}` };
|
|
1418
|
+
return await res.json();
|
|
1419
|
+
}));
|
|
1420
|
+
// Sort by combined score descending
|
|
1421
|
+
results.sort((a, b) => (b.combined?.score ?? 0) - (a.combined?.score ?? 0));
|
|
1422
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
1423
|
+
}
|
|
1424
|
+
catch (e) {
|
|
1425
|
+
return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true };
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
283
1428
|
// ─── Resources ──────────────────────────────────────────────────────────────
|
|
284
1429
|
server.resource('marketplace-info', 'obolos://marketplace/info', async () => ({
|
|
285
1430
|
contents: [
|
|
@@ -307,6 +1452,7 @@ async function main() {
|
|
|
307
1452
|
console.error(`[obolos-mcp] Connected to ${OBOLOS_API_URL}`);
|
|
308
1453
|
if (signer) {
|
|
309
1454
|
console.error(`[obolos-mcp] Wallet: ${signer.address}`);
|
|
1455
|
+
console.error(`[obolos-mcp] ACP contract: ${acpClient ? '0xaF3148696242F7Fb74893DC47690e37950807362 (Base)' : 'disabled'}`);
|
|
310
1456
|
}
|
|
311
1457
|
else {
|
|
312
1458
|
console.error('[obolos-mcp] No wallet configured. Run `npx @obolos_tech/cli setup` or set OBOLOS_PRIVATE_KEY.');
|