@yamo/mcp-server 1.3.10 → 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 +271 -248
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -47,19 +47,45 @@ 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
|
+
// 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
|
+
};
|
|
50
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
|
}
|
|
58
76
|
function validateEthereumAddress(address, fieldName) {
|
|
59
|
-
if (!address || !address.match(
|
|
77
|
+
if (!address || !address.match(VALIDATION_RULES.ETH_ADDRESS_PATTERN)) {
|
|
60
78
|
throw new Error(`${fieldName} must be a valid Ethereum address (0x + 40 hex characters)`);
|
|
61
79
|
}
|
|
62
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
|
+
}
|
|
63
89
|
function validateEnvironment() {
|
|
64
90
|
const requiredEnvVars = ['CONTRACT_ADDRESS', 'RPC_URL', 'PRIVATE_KEY'];
|
|
65
91
|
const missing = requiredEnvVars.filter(v => !process.env[v]);
|
|
@@ -275,6 +301,238 @@ class YamoMcpServer {
|
|
|
275
301
|
this.chain = new core_1.YamoChainClient();
|
|
276
302
|
this.setupHandlers();
|
|
277
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
|
+
};
|
|
278
536
|
setupHandlers() {
|
|
279
537
|
this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
280
538
|
tools: [SUBMIT_BLOCK_TOOL, GET_BLOCK_TOOL, GET_LATEST_BLOCK_TOOL, AUDIT_BLOCK_TOOL, VERIFY_BLOCK_TOOL],
|
|
@@ -282,261 +540,26 @@ class YamoMcpServer {
|
|
|
282
540
|
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
283
541
|
const { name, arguments: args } = request.params;
|
|
284
542
|
try {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
let processedFiles = files;
|
|
289
|
-
if (files && Array.isArray(files)) {
|
|
290
|
-
processedFiles = await Promise.all(files.map(async (file) => {
|
|
291
|
-
// Check if content is a file path that exists
|
|
292
|
-
if (typeof file.content === 'string' && fs_1.default.existsSync(file.content)) {
|
|
293
|
-
// Security: Resolve symlinks and restrict to cwd (prevents symlink attacks and TOCTOU)
|
|
294
|
-
const filePath = fs_1.default.realpathSync(file.content);
|
|
295
|
-
const allowedDir = fs_1.default.realpathSync(process.cwd());
|
|
296
|
-
// Check for symlinks
|
|
297
|
-
const stats = fs_1.default.lstatSync(file.content);
|
|
298
|
-
if (stats.isSymbolicLink()) {
|
|
299
|
-
throw new Error(`Symbolic links are not allowed: ${file.content}`);
|
|
300
|
-
}
|
|
301
|
-
if (!filePath.startsWith(allowedDir)) {
|
|
302
|
-
throw new Error(`File path outside allowed directory: ${file.content}`);
|
|
303
|
-
}
|
|
304
|
-
console.error(`[DEBUG] Auto-reading file from path: ${file.content}`);
|
|
305
|
-
return {
|
|
306
|
-
name: file.name,
|
|
307
|
-
content: await fs_1.default.promises.readFile(filePath, 'utf8')
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
// Otherwise use content as-is
|
|
311
|
-
return file;
|
|
312
|
-
}));
|
|
313
|
-
}
|
|
314
|
-
// Input validation (Part 3: Security Fixes)
|
|
315
|
-
validateBytes32(contentHash, "contentHash");
|
|
316
|
-
// Auto-fetch previousBlock if not provided
|
|
317
|
-
let resolvedPreviousBlock = previousBlock;
|
|
318
|
-
if (!resolvedPreviousBlock) {
|
|
319
|
-
console.error(`[INFO] No previousBlock provided, fetching latest block from chain...`);
|
|
320
|
-
// First, try the cache (most reliable for chain continuation)
|
|
321
|
-
if (this.latestContentHash) {
|
|
322
|
-
resolvedPreviousBlock = this.latestContentHash;
|
|
323
|
-
console.error(`[INFO] Using cached latest block's contentHash: ${resolvedPreviousBlock}`);
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
// Fallback to direct contract state read (reliable)
|
|
327
|
-
const latestHash = await this.chain.getLatestBlockHash();
|
|
328
|
-
if (latestHash && latestHash !== "0x0000000000000000000000000000000000000000000000000000000000000000") {
|
|
329
|
-
resolvedPreviousBlock = latestHash;
|
|
330
|
-
this.latestContentHash = latestHash; // Update cache
|
|
331
|
-
console.error(`[INFO] Using latest block's contentHash from contract: ${resolvedPreviousBlock}`);
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
// No blocks exist yet, use genesis
|
|
335
|
-
resolvedPreviousBlock = "0x0000000000000000000000000000000000000000000000000000000000000000";
|
|
336
|
-
console.error(`[INFO] No existing blocks found, using genesis`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
else if (previousBlock) {
|
|
341
|
-
validateBytes32(previousBlock, "previousBlock");
|
|
342
|
-
}
|
|
343
|
-
let ipfsCID = undefined;
|
|
344
|
-
if (content) {
|
|
345
|
-
ipfsCID = await this.ipfs.upload({
|
|
346
|
-
content,
|
|
347
|
-
files: processedFiles,
|
|
348
|
-
encryptionKey
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
const tx = await this.chain.submitBlock(blockId, resolvedPreviousBlock, contentHash, consensusType, ledger, ipfsCID);
|
|
352
|
-
const receipt = await tx.wait();
|
|
353
|
-
// Update cache with the new block's contentHash for chain continuation
|
|
354
|
-
this.latestContentHash = contentHash;
|
|
355
|
-
console.error(`[INFO] Updated latestContentHash cache: ${contentHash}`);
|
|
356
|
-
return {
|
|
357
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
358
|
-
success: true,
|
|
359
|
-
blockId,
|
|
360
|
-
transactionHash: tx.hash,
|
|
361
|
-
blockNumber: receipt.blockNumber,
|
|
362
|
-
gasUsed: receipt.gasUsed.toString(),
|
|
363
|
-
effectiveGasPrice: receipt.effectiveGasPrice?.toString(),
|
|
364
|
-
ipfsCID: ipfsCID || null,
|
|
365
|
-
previousBlock: resolvedPreviousBlock,
|
|
366
|
-
contractAddress: this.chain.getContractAddress(),
|
|
367
|
-
timestamp: new Date().toISOString()
|
|
368
|
-
}, null, 2) }],
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
if (name === "yamo_get_block") {
|
|
372
|
-
const { blockId } = args;
|
|
373
|
-
const block = await this.chain.getBlock(blockId);
|
|
374
|
-
if (!block) {
|
|
375
|
-
return {
|
|
376
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
377
|
-
success: false,
|
|
378
|
-
error: "Block not found on-chain",
|
|
379
|
-
blockId,
|
|
380
|
-
hint: "Verify the blockId or check if the block was submitted"
|
|
381
|
-
}, null, 2) }],
|
|
382
|
-
isError: true,
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
return {
|
|
386
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
387
|
-
success: true,
|
|
388
|
-
block: {
|
|
389
|
-
blockId: block.blockId,
|
|
390
|
-
previousBlock: block.previousBlock,
|
|
391
|
-
agentAddress: block.agentAddress,
|
|
392
|
-
contentHash: block.contentHash,
|
|
393
|
-
timestamp: block.timestamp,
|
|
394
|
-
timestampISO: new Date(block.timestamp * 1000).toISOString(),
|
|
395
|
-
consensusType: block.consensusType,
|
|
396
|
-
ledger: block.ledger,
|
|
397
|
-
ipfsCID: block.ipfsCID || null
|
|
398
|
-
}
|
|
399
|
-
}, null, 2) }],
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
if (name === "yamo_get_latest_block") {
|
|
403
|
-
const latestBlock = await this.chain.getLatestBlock();
|
|
404
|
-
if (!latestBlock) {
|
|
405
|
-
return {
|
|
406
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
407
|
-
success: false,
|
|
408
|
-
error: "No blocks found on-chain",
|
|
409
|
-
hint: "The chain may be empty. Try submitting a genesis block first."
|
|
410
|
-
}, null, 2) }],
|
|
411
|
-
isError: true,
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
return {
|
|
415
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
416
|
-
success: true,
|
|
417
|
-
block: {
|
|
418
|
-
blockId: latestBlock.blockId,
|
|
419
|
-
previousBlock: latestBlock.previousBlock,
|
|
420
|
-
agentAddress: latestBlock.agentAddress,
|
|
421
|
-
contentHash: latestBlock.contentHash,
|
|
422
|
-
timestamp: latestBlock.timestamp,
|
|
423
|
-
timestampISO: new Date(latestBlock.timestamp * 1000).toISOString(),
|
|
424
|
-
consensusType: latestBlock.consensusType,
|
|
425
|
-
ledger: latestBlock.ledger,
|
|
426
|
-
ipfsCID: latestBlock.ipfsCID || null
|
|
427
|
-
}
|
|
428
|
-
}, null, 2) }],
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
if (name === "yamo_audit_block") {
|
|
432
|
-
const { blockId, encryptionKey } = args;
|
|
433
|
-
// Get block from chain
|
|
434
|
-
const block = await this.chain.getBlock(blockId);
|
|
435
|
-
if (!block) {
|
|
436
|
-
return {
|
|
437
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
438
|
-
verified: false,
|
|
439
|
-
error: "Block not found on-chain",
|
|
440
|
-
blockId,
|
|
441
|
-
hint: "Cannot audit non-existent block"
|
|
442
|
-
}, null, 2) }],
|
|
443
|
-
isError: true,
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
// If no IPFS CID, can't audit content
|
|
447
|
-
if (!block.ipfsCID) {
|
|
448
|
-
return {
|
|
449
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
450
|
-
verified: null, // Cannot verify without IPFS
|
|
451
|
-
onChainHash: block.contentHash,
|
|
452
|
-
ipfsCID: null,
|
|
453
|
-
note: "V1 block with no IPFS CID - cannot audit actual content",
|
|
454
|
-
blockId,
|
|
455
|
-
agentAddress: block.agentAddress,
|
|
456
|
-
timestamp: block.timestamp
|
|
457
|
-
}, null, 2) }],
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
// Download and verify from IPFS
|
|
461
|
-
try {
|
|
462
|
-
const bundle = await this.ipfs.downloadBundle(block.ipfsCID, encryptionKey);
|
|
463
|
-
const computedHash = "0x" + crypto_1.default.createHash("sha256").update(bundle.block).digest("hex");
|
|
464
|
-
const verified = computedHash === block.contentHash;
|
|
465
|
-
return {
|
|
466
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
467
|
-
verified,
|
|
468
|
-
blockId,
|
|
469
|
-
onChainHash: block.contentHash,
|
|
470
|
-
computedHash,
|
|
471
|
-
ipfsCID: block.ipfsCID,
|
|
472
|
-
agentAddress: block.agentAddress,
|
|
473
|
-
timestamp: block.timestamp,
|
|
474
|
-
timestampISO: new Date(block.timestamp * 1000).toISOString(),
|
|
475
|
-
consensusType: block.consensusType,
|
|
476
|
-
ledger: block.ledger,
|
|
477
|
-
contentPreview: bundle.block.substring(0, 500) + (bundle.block.length > 500 ? "..." : ""),
|
|
478
|
-
contentLength: bundle.block.length,
|
|
479
|
-
artifactFiles: Object.keys(bundle.files),
|
|
480
|
-
wasEncrypted: !!encryptionKey
|
|
481
|
-
}, null, 2) }],
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
catch (error) {
|
|
485
|
-
// Enhanced error messages
|
|
486
|
-
let errorType = "unknown";
|
|
487
|
-
let hint = "";
|
|
488
|
-
if (error.message.includes("encrypted") && !encryptionKey) {
|
|
489
|
-
errorType = "missing_key";
|
|
490
|
-
hint = "This bundle is encrypted. Provide encryptionKey to audit.";
|
|
491
|
-
}
|
|
492
|
-
else if (error.message.includes("Decryption failed") || error.message.includes("decrypt")) {
|
|
493
|
-
errorType = "decryption_failed";
|
|
494
|
-
hint = "The provided encryption key may be incorrect.";
|
|
495
|
-
}
|
|
496
|
-
else if (error.message.includes("not found on-chain")) {
|
|
497
|
-
errorType = "block_not_found";
|
|
498
|
-
hint = "Verify the blockId was submitted correctly.";
|
|
499
|
-
}
|
|
500
|
-
return {
|
|
501
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
502
|
-
verified: false,
|
|
503
|
-
error: error.message,
|
|
504
|
-
errorType,
|
|
505
|
-
hint,
|
|
506
|
-
blockId
|
|
507
|
-
}, null, 2) }],
|
|
508
|
-
isError: true,
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
if (name === "yamo_verify_block") {
|
|
513
|
-
const { blockId, contentHash } = args;
|
|
514
|
-
const contract = this.chain.getContract(false);
|
|
515
|
-
const hashBytes = contentHash.startsWith("0x") ? contentHash : `0x${contentHash}`;
|
|
516
|
-
const isValid = await contract.verifyBlock(blockId, hashBytes);
|
|
517
|
-
return {
|
|
518
|
-
content: [{ type: "text", text: isValid ? "VERIFIED" : "FAILED" }],
|
|
519
|
-
};
|
|
543
|
+
const handler = this.toolHandlers[name];
|
|
544
|
+
if (!handler) {
|
|
545
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
520
546
|
}
|
|
521
|
-
|
|
547
|
+
return await handler(args);
|
|
522
548
|
}
|
|
523
549
|
catch (error) {
|
|
524
|
-
return {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
}, null, 2) }],
|
|
531
|
-
isError: true,
|
|
532
|
-
};
|
|
550
|
+
return this.createErrorResponse({
|
|
551
|
+
success: false,
|
|
552
|
+
error: error.message,
|
|
553
|
+
tool: name,
|
|
554
|
+
timestamp: new Date().toISOString()
|
|
555
|
+
});
|
|
533
556
|
}
|
|
534
557
|
});
|
|
535
558
|
}
|
|
536
559
|
async run() {
|
|
537
560
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
538
561
|
await this.server.connect(transport);
|
|
539
|
-
|
|
562
|
+
this.log('INFO', `YAMO MCP Server v${pkg.version} running on stdio`);
|
|
540
563
|
}
|
|
541
564
|
}
|
|
542
565
|
const server = new YamoMcpServer();
|