@yamo/mcp-server 1.3.9 → 1.3.11
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/index.js +288 -243
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -47,14 +47,55 @@ const crypto_1 = __importDefault(require("crypto"));
|
|
|
47
47
|
const path_1 = __importDefault(require("path"));
|
|
48
48
|
const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../package.json'), 'utf8'));
|
|
49
49
|
dotenv.config();
|
|
50
|
-
//
|
|
50
|
+
// Constants
|
|
51
|
+
const GENESIS_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000";
|
|
52
|
+
const TOOL_NAMES = {
|
|
53
|
+
SUBMIT_BLOCK: "yamo_submit_block",
|
|
54
|
+
GET_BLOCK: "yamo_get_block",
|
|
55
|
+
GET_LATEST_BLOCK: "yamo_get_latest_block",
|
|
56
|
+
AUDIT_BLOCK: "yamo_audit_block",
|
|
57
|
+
VERIFY_BLOCK: "yamo_verify_block",
|
|
58
|
+
};
|
|
59
|
+
const LOG_PREFIX = {
|
|
60
|
+
DEBUG: "[DEBUG]",
|
|
61
|
+
INFO: "[INFO]",
|
|
62
|
+
ERROR: "[ERROR]",
|
|
63
|
+
};
|
|
64
|
+
const VALIDATION_RULES = {
|
|
65
|
+
BYTES32_PATTERN: /^0x[a-fA-F0-9]{64}$/,
|
|
66
|
+
ETH_ADDRESS_PATTERN: /^0x[a-fA-F0-9]{40}$/,
|
|
67
|
+
};
|
|
68
|
+
// Validation helpers
|
|
51
69
|
function validateBytes32(value, fieldName) {
|
|
52
|
-
if (!value.match(
|
|
70
|
+
if (!value.match(VALIDATION_RULES.BYTES32_PATTERN)) {
|
|
53
71
|
throw new Error(`${fieldName} must be a valid bytes32 hash (0x + 64 hex chars). ` +
|
|
54
72
|
`Received: ${value.substring(0, 20)}...` +
|
|
55
73
|
`\nDo NOT include algorithm prefixes like "sha256:"`);
|
|
56
74
|
}
|
|
57
75
|
}
|
|
76
|
+
function validateEthereumAddress(address, fieldName) {
|
|
77
|
+
if (!address || !address.match(VALIDATION_RULES.ETH_ADDRESS_PATTERN)) {
|
|
78
|
+
throw new Error(`${fieldName} must be a valid Ethereum address (0x + 40 hex characters)`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function validateBlockId(blockId) {
|
|
82
|
+
if (!blockId)
|
|
83
|
+
throw new Error("blockId is required");
|
|
84
|
+
const parts = blockId.split('_');
|
|
85
|
+
if (parts.length < 2) {
|
|
86
|
+
throw new Error(`blockId must follow format {origin}_{workflow} (e.g., 'claude_chain'). Received: ${blockId}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function validateEnvironment() {
|
|
90
|
+
const requiredEnvVars = ['CONTRACT_ADDRESS', 'RPC_URL', 'PRIVATE_KEY'];
|
|
91
|
+
const missing = requiredEnvVars.filter(v => !process.env[v]);
|
|
92
|
+
if (missing.length > 0) {
|
|
93
|
+
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
// Validate contract address format
|
|
96
|
+
const contractAddress = process.env.CONTRACT_ADDRESS;
|
|
97
|
+
validateEthereumAddress(contractAddress, 'CONTRACT_ADDRESS');
|
|
98
|
+
}
|
|
58
99
|
const SUBMIT_BLOCK_TOOL = {
|
|
59
100
|
name: "yamo_submit_block",
|
|
60
101
|
description: `Submits a YAMO block to the YAMORegistry smart contract.
|
|
@@ -253,11 +294,245 @@ class YamoMcpServer {
|
|
|
253
294
|
// Cache for chain continuation: latest submitted block's contentHash
|
|
254
295
|
latestContentHash = null;
|
|
255
296
|
constructor() {
|
|
297
|
+
// Validate environment variables at startup
|
|
298
|
+
validateEnvironment();
|
|
256
299
|
this.server = new index_js_1.Server({ name: "yamo", version: pkg.version }, { capabilities: { tools: {} } });
|
|
257
300
|
this.ipfs = new core_1.IpfsManager();
|
|
258
301
|
this.chain = new core_1.YamoChainClient();
|
|
259
302
|
this.setupHandlers();
|
|
260
303
|
}
|
|
304
|
+
// Response formatting helpers
|
|
305
|
+
createSuccessResponse(data) {
|
|
306
|
+
return {
|
|
307
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
createErrorResponse(error, isError = true) {
|
|
311
|
+
return {
|
|
312
|
+
content: [{ type: "text", text: JSON.stringify(error, null, 2) }],
|
|
313
|
+
isError,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
createTextResponse(text) {
|
|
317
|
+
return { content: [{ type: "text", text }] };
|
|
318
|
+
}
|
|
319
|
+
// Logging helper
|
|
320
|
+
log(level, message) {
|
|
321
|
+
const prefix = LOG_PREFIX[level];
|
|
322
|
+
console.error(`${prefix} ${message}`);
|
|
323
|
+
}
|
|
324
|
+
// Cache update helper
|
|
325
|
+
updateLatestBlockCache(contentHash) {
|
|
326
|
+
this.latestContentHash = contentHash;
|
|
327
|
+
this.log('INFO', `Updated latestContentHash cache: ${contentHash}`);
|
|
328
|
+
}
|
|
329
|
+
// Block formatting helper
|
|
330
|
+
formatBlockResponse(block) {
|
|
331
|
+
return {
|
|
332
|
+
blockId: block.blockId,
|
|
333
|
+
previousBlock: block.previousBlock,
|
|
334
|
+
agentAddress: block.agentAddress,
|
|
335
|
+
contentHash: block.contentHash,
|
|
336
|
+
timestamp: block.timestamp,
|
|
337
|
+
timestampISO: new Date(block.timestamp * 1000).toISOString(),
|
|
338
|
+
consensusType: block.consensusType,
|
|
339
|
+
ledger: block.ledger,
|
|
340
|
+
ipfsCID: block.ipfsCID || null
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
// File security validation
|
|
344
|
+
validateFileSecurity(filePath) {
|
|
345
|
+
const realPath = fs_1.default.realpathSync(filePath);
|
|
346
|
+
const allowedDir = fs_1.default.realpathSync(process.cwd());
|
|
347
|
+
const stats = fs_1.default.lstatSync(filePath);
|
|
348
|
+
if (stats.isSymbolicLink()) {
|
|
349
|
+
throw new Error(`Symbolic links are not allowed: ${filePath}`);
|
|
350
|
+
}
|
|
351
|
+
if (!realPath.startsWith(allowedDir)) {
|
|
352
|
+
throw new Error(`File path outside allowed directory: ${filePath}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// File processing helper
|
|
356
|
+
async processSingleFile(file) {
|
|
357
|
+
if (typeof file.content === 'string' && fs_1.default.existsSync(file.content)) {
|
|
358
|
+
this.validateFileSecurity(file.content);
|
|
359
|
+
this.log('DEBUG', `Auto-reading file from path: ${file.content}`);
|
|
360
|
+
const content = await fs_1.default.promises.readFile(fs_1.default.realpathSync(file.content), 'utf8');
|
|
361
|
+
return { name: file.name, content };
|
|
362
|
+
}
|
|
363
|
+
return file;
|
|
364
|
+
}
|
|
365
|
+
// Previous block resolution helper
|
|
366
|
+
async resolvePreviousBlock(previousBlock) {
|
|
367
|
+
if (previousBlock)
|
|
368
|
+
return previousBlock;
|
|
369
|
+
this.log('INFO', 'No previousBlock provided, fetching latest block from chain...');
|
|
370
|
+
if (this.latestContentHash) {
|
|
371
|
+
this.log('INFO', `Using cached latest block's contentHash: ${this.latestContentHash}`);
|
|
372
|
+
return this.latestContentHash;
|
|
373
|
+
}
|
|
374
|
+
const latestHash = await this.chain.getLatestBlockHash();
|
|
375
|
+
if (latestHash && latestHash !== GENESIS_HASH) {
|
|
376
|
+
this.latestContentHash = latestHash;
|
|
377
|
+
this.log('INFO', `Using latest block's contentHash from contract: ${latestHash}`);
|
|
378
|
+
return latestHash;
|
|
379
|
+
}
|
|
380
|
+
this.log('INFO', 'No existing blocks found, using genesis');
|
|
381
|
+
return GENESIS_HASH;
|
|
382
|
+
}
|
|
383
|
+
async handleSubmitBlock(args) {
|
|
384
|
+
const { blockId, previousBlock, contentHash, consensusType, ledger, content, files, encryptionKey } = args;
|
|
385
|
+
// Validate inputs
|
|
386
|
+
validateBlockId(blockId);
|
|
387
|
+
validateBytes32(contentHash, "contentHash");
|
|
388
|
+
if (previousBlock) {
|
|
389
|
+
validateBytes32(previousBlock, "previousBlock");
|
|
390
|
+
}
|
|
391
|
+
// Process files with security validation
|
|
392
|
+
const processedFiles = files && Array.isArray(files)
|
|
393
|
+
? await Promise.all(files.map(f => this.processSingleFile(f)))
|
|
394
|
+
: files;
|
|
395
|
+
// Resolve previous block hash
|
|
396
|
+
const resolvedPreviousBlock = await this.resolvePreviousBlock(previousBlock);
|
|
397
|
+
// Upload to IPFS if content provided
|
|
398
|
+
let ipfsCID = undefined;
|
|
399
|
+
if (content) {
|
|
400
|
+
ipfsCID = await this.ipfs.upload({
|
|
401
|
+
content,
|
|
402
|
+
files: processedFiles,
|
|
403
|
+
encryptionKey
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
// Submit to blockchain
|
|
407
|
+
const tx = await this.chain.submitBlock(blockId, resolvedPreviousBlock, contentHash, consensusType, ledger, ipfsCID);
|
|
408
|
+
const receipt = await tx.wait();
|
|
409
|
+
// Update cache
|
|
410
|
+
this.updateLatestBlockCache(contentHash);
|
|
411
|
+
return this.createSuccessResponse({
|
|
412
|
+
success: true,
|
|
413
|
+
blockId,
|
|
414
|
+
transactionHash: tx.hash,
|
|
415
|
+
blockNumber: receipt.blockNumber,
|
|
416
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
417
|
+
effectiveGasPrice: receipt.effectiveGasPrice?.toString(),
|
|
418
|
+
ipfsCID: ipfsCID || null,
|
|
419
|
+
previousBlock: resolvedPreviousBlock,
|
|
420
|
+
contractAddress: this.chain.getContractAddress(),
|
|
421
|
+
timestamp: new Date().toISOString()
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
async handleGetBlock(args) {
|
|
425
|
+
const { blockId } = args;
|
|
426
|
+
const block = await this.chain.getBlock(blockId);
|
|
427
|
+
if (!block) {
|
|
428
|
+
return this.createErrorResponse({
|
|
429
|
+
success: false,
|
|
430
|
+
error: "Block not found on-chain",
|
|
431
|
+
blockId,
|
|
432
|
+
hint: "Verify the blockId or check if the block was submitted"
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return this.createSuccessResponse({
|
|
436
|
+
success: true,
|
|
437
|
+
block: this.formatBlockResponse(block)
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
async handleGetLatestBlock() {
|
|
441
|
+
const latestBlock = await this.chain.getLatestBlock();
|
|
442
|
+
if (!latestBlock) {
|
|
443
|
+
return this.createErrorResponse({
|
|
444
|
+
success: false,
|
|
445
|
+
error: "No blocks found on-chain",
|
|
446
|
+
hint: "The chain may be empty. Try submitting a genesis block first."
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return this.createSuccessResponse({
|
|
450
|
+
success: true,
|
|
451
|
+
block: this.formatBlockResponse(latestBlock)
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async handleAuditBlock(args) {
|
|
455
|
+
const { blockId, encryptionKey } = args;
|
|
456
|
+
const block = await this.chain.getBlock(blockId);
|
|
457
|
+
if (!block) {
|
|
458
|
+
return this.createErrorResponse({
|
|
459
|
+
verified: false,
|
|
460
|
+
error: "Block not found on-chain",
|
|
461
|
+
blockId,
|
|
462
|
+
hint: "Cannot audit non-existent block"
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (!block.ipfsCID) {
|
|
466
|
+
return this.createSuccessResponse({
|
|
467
|
+
verified: null,
|
|
468
|
+
onChainHash: block.contentHash,
|
|
469
|
+
ipfsCID: null,
|
|
470
|
+
note: "V1 block with no IPFS CID - cannot audit actual content",
|
|
471
|
+
blockId,
|
|
472
|
+
agentAddress: block.agentAddress,
|
|
473
|
+
timestamp: block.timestamp
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
const bundle = await this.ipfs.downloadBundle(block.ipfsCID, encryptionKey);
|
|
478
|
+
const computedHash = "0x" + crypto_1.default.createHash("sha256").update(bundle.block).digest("hex");
|
|
479
|
+
const verified = computedHash === block.contentHash;
|
|
480
|
+
return this.createSuccessResponse({
|
|
481
|
+
verified,
|
|
482
|
+
blockId,
|
|
483
|
+
onChainHash: block.contentHash,
|
|
484
|
+
computedHash,
|
|
485
|
+
ipfsCID: block.ipfsCID,
|
|
486
|
+
agentAddress: block.agentAddress,
|
|
487
|
+
timestamp: block.timestamp,
|
|
488
|
+
timestampISO: new Date(block.timestamp * 1000).toISOString(),
|
|
489
|
+
consensusType: block.consensusType,
|
|
490
|
+
ledger: block.ledger,
|
|
491
|
+
contentPreview: bundle.block.substring(0, 500) + (bundle.block.length > 500 ? "..." : ""),
|
|
492
|
+
contentLength: bundle.block.length,
|
|
493
|
+
artifactFiles: Object.keys(bundle.files),
|
|
494
|
+
wasEncrypted: !!encryptionKey
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
let errorType = "unknown";
|
|
499
|
+
let hint = "";
|
|
500
|
+
if (error.message.includes("encrypted") && !encryptionKey) {
|
|
501
|
+
errorType = "missing_key";
|
|
502
|
+
hint = "This bundle is encrypted. Provide encryptionKey to audit.";
|
|
503
|
+
}
|
|
504
|
+
else if (error.message.includes("Decryption failed") || error.message.includes("decrypt")) {
|
|
505
|
+
errorType = "decryption_failed";
|
|
506
|
+
hint = "The provided encryption key may be incorrect.";
|
|
507
|
+
}
|
|
508
|
+
else if (error.message.includes("not found on-chain")) {
|
|
509
|
+
errorType = "block_not_found";
|
|
510
|
+
hint = "Verify the blockId was submitted correctly.";
|
|
511
|
+
}
|
|
512
|
+
return this.createErrorResponse({
|
|
513
|
+
verified: false,
|
|
514
|
+
error: error.message,
|
|
515
|
+
errorType,
|
|
516
|
+
hint,
|
|
517
|
+
blockId
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async handleVerifyBlock(args) {
|
|
522
|
+
const { blockId, contentHash } = args;
|
|
523
|
+
const contract = this.chain.getContract(false);
|
|
524
|
+
const hashBytes = contentHash.startsWith("0x") ? contentHash : `0x${contentHash}`;
|
|
525
|
+
const isValid = await contract.verifyBlock(blockId, hashBytes);
|
|
526
|
+
return this.createTextResponse(isValid ? "VERIFIED" : "FAILED");
|
|
527
|
+
}
|
|
528
|
+
// Tool handler registry
|
|
529
|
+
toolHandlers = {
|
|
530
|
+
[TOOL_NAMES.SUBMIT_BLOCK]: (args) => this.handleSubmitBlock(args),
|
|
531
|
+
[TOOL_NAMES.GET_BLOCK]: (args) => this.handleGetBlock(args),
|
|
532
|
+
[TOOL_NAMES.GET_LATEST_BLOCK]: () => this.handleGetLatestBlock(),
|
|
533
|
+
[TOOL_NAMES.AUDIT_BLOCK]: (args) => this.handleAuditBlock(args),
|
|
534
|
+
[TOOL_NAMES.VERIFY_BLOCK]: (args) => this.handleVerifyBlock(args),
|
|
535
|
+
};
|
|
261
536
|
setupHandlers() {
|
|
262
537
|
this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
263
538
|
tools: [SUBMIT_BLOCK_TOOL, GET_BLOCK_TOOL, GET_LATEST_BLOCK_TOOL, AUDIT_BLOCK_TOOL, VERIFY_BLOCK_TOOL],
|
|
@@ -265,256 +540,26 @@ class YamoMcpServer {
|
|
|
265
540
|
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
266
541
|
const { name, arguments: args } = request.params;
|
|
267
542
|
try {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
let processedFiles = files;
|
|
272
|
-
if (files && Array.isArray(files)) {
|
|
273
|
-
processedFiles = files.map((file) => {
|
|
274
|
-
// Check if content is a file path that exists
|
|
275
|
-
if (typeof file.content === 'string' && fs_1.default.existsSync(file.content)) {
|
|
276
|
-
// Security: Resolve to absolute path and restrict to cwd (Part 3: Security Fixes)
|
|
277
|
-
const filePath = path_1.default.resolve(file.content);
|
|
278
|
-
const allowedDir = process.cwd();
|
|
279
|
-
if (!filePath.startsWith(allowedDir)) {
|
|
280
|
-
throw new Error(`File path outside allowed directory: ${file.content}`);
|
|
281
|
-
}
|
|
282
|
-
console.error(`[DEBUG] Auto-reading file from path: ${file.content}`);
|
|
283
|
-
return {
|
|
284
|
-
name: file.name,
|
|
285
|
-
content: fs_1.default.readFileSync(filePath, 'utf8')
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
// Otherwise use content as-is
|
|
289
|
-
return file;
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
// Input validation (Part 3: Security Fixes)
|
|
293
|
-
validateBytes32(contentHash, "contentHash");
|
|
294
|
-
// Auto-fetch previousBlock if not provided
|
|
295
|
-
let resolvedPreviousBlock = previousBlock;
|
|
296
|
-
if (!resolvedPreviousBlock) {
|
|
297
|
-
console.error(`[INFO] No previousBlock provided, fetching latest block from chain...`);
|
|
298
|
-
// First, try the cache (most reliable for chain continuation)
|
|
299
|
-
if (this.latestContentHash) {
|
|
300
|
-
resolvedPreviousBlock = this.latestContentHash;
|
|
301
|
-
console.error(`[INFO] Using cached latest block's contentHash: ${resolvedPreviousBlock}`);
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
// Fallback to direct contract state read (reliable)
|
|
305
|
-
const latestHash = await this.chain.getLatestBlockHash();
|
|
306
|
-
if (latestHash && latestHash !== "0x0000000000000000000000000000000000000000000000000000000000000000") {
|
|
307
|
-
resolvedPreviousBlock = latestHash;
|
|
308
|
-
this.latestContentHash = latestHash; // Update cache
|
|
309
|
-
console.error(`[INFO] Using latest block's contentHash from contract: ${resolvedPreviousBlock}`);
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
// No blocks exist yet, use genesis
|
|
313
|
-
resolvedPreviousBlock = "0x0000000000000000000000000000000000000000000000000000000000000000";
|
|
314
|
-
console.error(`[INFO] No existing blocks found, using genesis`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
validateBytes32(previousBlock, "previousBlock");
|
|
320
|
-
}
|
|
321
|
-
let ipfsCID = undefined;
|
|
322
|
-
if (content) {
|
|
323
|
-
ipfsCID = await this.ipfs.upload({
|
|
324
|
-
content,
|
|
325
|
-
files: processedFiles,
|
|
326
|
-
encryptionKey
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
const tx = await this.chain.submitBlock(blockId, resolvedPreviousBlock, contentHash, consensusType, ledger, ipfsCID);
|
|
330
|
-
const receipt = await tx.wait();
|
|
331
|
-
// Update cache with the new block's contentHash for chain continuation
|
|
332
|
-
this.latestContentHash = contentHash;
|
|
333
|
-
console.error(`[INFO] Updated latestContentHash cache: ${contentHash}`);
|
|
334
|
-
return {
|
|
335
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
336
|
-
success: true,
|
|
337
|
-
blockId,
|
|
338
|
-
transactionHash: tx.hash,
|
|
339
|
-
blockNumber: receipt.blockNumber,
|
|
340
|
-
gasUsed: receipt.gasUsed.toString(),
|
|
341
|
-
effectiveGasPrice: receipt.effectiveGasPrice?.toString(),
|
|
342
|
-
ipfsCID: ipfsCID || null,
|
|
343
|
-
previousBlock: resolvedPreviousBlock,
|
|
344
|
-
contractAddress: this.chain.getContractAddress(),
|
|
345
|
-
timestamp: new Date().toISOString()
|
|
346
|
-
}, null, 2) }],
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
if (name === "yamo_get_block") {
|
|
350
|
-
const { blockId } = args;
|
|
351
|
-
const block = await this.chain.getBlock(blockId);
|
|
352
|
-
if (!block) {
|
|
353
|
-
return {
|
|
354
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
355
|
-
success: false,
|
|
356
|
-
error: "Block not found on-chain",
|
|
357
|
-
blockId,
|
|
358
|
-
hint: "Verify the blockId or check if the block was submitted"
|
|
359
|
-
}, null, 2) }],
|
|
360
|
-
isError: false,
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
return {
|
|
364
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
365
|
-
success: true,
|
|
366
|
-
block: {
|
|
367
|
-
blockId: block.blockId,
|
|
368
|
-
previousBlock: block.previousBlock,
|
|
369
|
-
agentAddress: block.agentAddress,
|
|
370
|
-
contentHash: block.contentHash,
|
|
371
|
-
timestamp: block.timestamp,
|
|
372
|
-
timestampISO: new Date(block.timestamp * 1000).toISOString(),
|
|
373
|
-
consensusType: block.consensusType,
|
|
374
|
-
ledger: block.ledger,
|
|
375
|
-
ipfsCID: block.ipfsCID || null
|
|
376
|
-
}
|
|
377
|
-
}, null, 2) }],
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
if (name === "yamo_get_latest_block") {
|
|
381
|
-
const latestBlock = await this.chain.getLatestBlock();
|
|
382
|
-
if (!latestBlock) {
|
|
383
|
-
return {
|
|
384
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
385
|
-
success: false,
|
|
386
|
-
error: "No blocks found on-chain",
|
|
387
|
-
hint: "The chain may be empty. Try submitting a genesis block first."
|
|
388
|
-
}, null, 2) }],
|
|
389
|
-
isError: false,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
return {
|
|
393
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
394
|
-
success: true,
|
|
395
|
-
block: {
|
|
396
|
-
blockId: latestBlock.blockId,
|
|
397
|
-
previousBlock: latestBlock.previousBlock,
|
|
398
|
-
agentAddress: latestBlock.agentAddress,
|
|
399
|
-
contentHash: latestBlock.contentHash,
|
|
400
|
-
timestamp: latestBlock.timestamp,
|
|
401
|
-
timestampISO: new Date(latestBlock.timestamp * 1000).toISOString(),
|
|
402
|
-
consensusType: latestBlock.consensusType,
|
|
403
|
-
ledger: latestBlock.ledger,
|
|
404
|
-
ipfsCID: latestBlock.ipfsCID || null
|
|
405
|
-
}
|
|
406
|
-
}, null, 2) }],
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
if (name === "yamo_audit_block") {
|
|
410
|
-
const { blockId, encryptionKey } = args;
|
|
411
|
-
// Get block from chain
|
|
412
|
-
const block = await this.chain.getBlock(blockId);
|
|
413
|
-
if (!block) {
|
|
414
|
-
return {
|
|
415
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
416
|
-
verified: false,
|
|
417
|
-
error: "Block not found on-chain",
|
|
418
|
-
blockId,
|
|
419
|
-
hint: "Cannot audit non-existent block"
|
|
420
|
-
}, null, 2) }],
|
|
421
|
-
isError: false,
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
// If no IPFS CID, can't audit content
|
|
425
|
-
if (!block.ipfsCID) {
|
|
426
|
-
return {
|
|
427
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
428
|
-
verified: null, // Cannot verify without IPFS
|
|
429
|
-
onChainHash: block.contentHash,
|
|
430
|
-
ipfsCID: null,
|
|
431
|
-
note: "V1 block with no IPFS CID - cannot audit actual content",
|
|
432
|
-
blockId,
|
|
433
|
-
agentAddress: block.agentAddress,
|
|
434
|
-
timestamp: block.timestamp
|
|
435
|
-
}, null, 2) }],
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
// Download and verify from IPFS
|
|
439
|
-
try {
|
|
440
|
-
const bundle = await this.ipfs.downloadBundle(block.ipfsCID, encryptionKey);
|
|
441
|
-
const computedHash = "0x" + crypto_1.default.createHash("sha256").update(bundle.block).digest("hex");
|
|
442
|
-
const verified = computedHash === block.contentHash;
|
|
443
|
-
return {
|
|
444
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
445
|
-
verified,
|
|
446
|
-
blockId,
|
|
447
|
-
onChainHash: block.contentHash,
|
|
448
|
-
computedHash,
|
|
449
|
-
ipfsCID: block.ipfsCID,
|
|
450
|
-
agentAddress: block.agentAddress,
|
|
451
|
-
timestamp: block.timestamp,
|
|
452
|
-
timestampISO: new Date(block.timestamp * 1000).toISOString(),
|
|
453
|
-
consensusType: block.consensusType,
|
|
454
|
-
ledger: block.ledger,
|
|
455
|
-
contentPreview: bundle.block.substring(0, 500) + (bundle.block.length > 500 ? "..." : ""),
|
|
456
|
-
contentLength: bundle.block.length,
|
|
457
|
-
artifactFiles: Object.keys(bundle.files),
|
|
458
|
-
wasEncrypted: !!encryptionKey
|
|
459
|
-
}, null, 2) }],
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
catch (error) {
|
|
463
|
-
// Enhanced error messages
|
|
464
|
-
let errorType = "unknown";
|
|
465
|
-
let hint = "";
|
|
466
|
-
if (error.message.includes("encrypted") && !encryptionKey) {
|
|
467
|
-
errorType = "missing_key";
|
|
468
|
-
hint = "This bundle is encrypted. Provide encryptionKey to audit.";
|
|
469
|
-
}
|
|
470
|
-
else if (error.message.includes("Decryption failed") || error.message.includes("decrypt")) {
|
|
471
|
-
errorType = "decryption_failed";
|
|
472
|
-
hint = "The provided encryption key may be incorrect.";
|
|
473
|
-
}
|
|
474
|
-
else if (error.message.includes("not found on-chain")) {
|
|
475
|
-
errorType = "block_not_found";
|
|
476
|
-
hint = "Verify the blockId was submitted correctly.";
|
|
477
|
-
}
|
|
478
|
-
return {
|
|
479
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
480
|
-
verified: false,
|
|
481
|
-
error: error.message,
|
|
482
|
-
errorType,
|
|
483
|
-
hint,
|
|
484
|
-
blockId
|
|
485
|
-
}, null, 2) }],
|
|
486
|
-
isError: true,
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
if (name === "yamo_verify_block") {
|
|
491
|
-
const { blockId, contentHash } = args;
|
|
492
|
-
const contract = this.chain.getContract(false);
|
|
493
|
-
const hashBytes = contentHash.startsWith("0x") ? contentHash : `0x${contentHash}`;
|
|
494
|
-
const isValid = await contract.verifyBlock(blockId, hashBytes);
|
|
495
|
-
return {
|
|
496
|
-
content: [{ type: "text", text: isValid ? "VERIFIED" : "FAILED" }],
|
|
497
|
-
};
|
|
543
|
+
const handler = this.toolHandlers[name];
|
|
544
|
+
if (!handler) {
|
|
545
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
498
546
|
}
|
|
499
|
-
|
|
547
|
+
return await handler(args);
|
|
500
548
|
}
|
|
501
549
|
catch (error) {
|
|
502
|
-
return {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}, null, 2) }],
|
|
509
|
-
isError: true,
|
|
510
|
-
};
|
|
550
|
+
return this.createErrorResponse({
|
|
551
|
+
success: false,
|
|
552
|
+
error: error.message,
|
|
553
|
+
tool: name,
|
|
554
|
+
timestamp: new Date().toISOString()
|
|
555
|
+
});
|
|
511
556
|
}
|
|
512
557
|
});
|
|
513
558
|
}
|
|
514
559
|
async run() {
|
|
515
560
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
516
561
|
await this.server.connect(transport);
|
|
517
|
-
|
|
562
|
+
this.log('INFO', `YAMO MCP Server v${pkg.version} running on stdio`);
|
|
518
563
|
}
|
|
519
564
|
}
|
|
520
565
|
const server = new YamoMcpServer();
|