@yamo/mcp-server 1.3.1 → 1.3.4
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 +126 -12
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -47,6 +47,14 @@ 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
|
+
// Validation helper for bytes32 hashes (Part 3: Security Fixes)
|
|
51
|
+
function validateBytes32(value, fieldName) {
|
|
52
|
+
if (!value.match(/^0x[a-fA-F0-9]{64}$/)) {
|
|
53
|
+
throw new Error(`${fieldName} must be a valid bytes32 hash (0x + 64 hex chars). ` +
|
|
54
|
+
`Received: ${value.substring(0, 20)}...` +
|
|
55
|
+
`\nDo NOT include algorithm prefixes like "sha256:"`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
50
58
|
const SUBMIT_BLOCK_TOOL = {
|
|
51
59
|
name: "yamo_submit_block",
|
|
52
60
|
description: `Submits a YAMO block to the YAMORegistry smart contract.
|
|
@@ -58,12 +66,15 @@ const SUBMIT_BLOCK_TOOL = {
|
|
|
58
66
|
- contentHash: Must be a valid bytes32 hash (32 bytes = 64 hex characters)
|
|
59
67
|
• Format: "0x" followed by exactly 64 hexadecimal characters
|
|
60
68
|
• Do NOT include algorithm prefixes (e.g., "sha256:")
|
|
61
|
-
- previousBlock:
|
|
69
|
+
- previousBlock: Content hash of parent block.
|
|
70
|
+
• If omitted, automatically fetches the latest block's contentHash
|
|
71
|
+
• For genesis block: 0x0000000000000000000000000000000000000000000000000000000000000000
|
|
62
72
|
|
|
63
73
|
**Transaction Flow:**
|
|
64
|
-
1.
|
|
65
|
-
2.
|
|
66
|
-
3.
|
|
74
|
+
1. If previousBlock omitted, fetches latest block from chain automatically
|
|
75
|
+
2. Content and files are uploaded to IPFS (if provided)
|
|
76
|
+
3. Block is submitted to smart contract with hash reference
|
|
77
|
+
4. Returns transaction hash and IPFS CID`,
|
|
67
78
|
inputSchema: {
|
|
68
79
|
type: "object",
|
|
69
80
|
properties: {
|
|
@@ -74,7 +85,7 @@ const SUBMIT_BLOCK_TOOL = {
|
|
|
74
85
|
previousBlock: {
|
|
75
86
|
type: "string",
|
|
76
87
|
pattern: "^0x[a-fA-F0-9]{64}$",
|
|
77
|
-
description: "Content hash of parent block.
|
|
88
|
+
description: "Content hash of parent block. If omitted, auto-fetched from latest block. Use 0x0000...0000 for genesis."
|
|
78
89
|
},
|
|
79
90
|
contentHash: {
|
|
80
91
|
type: "string",
|
|
@@ -111,7 +122,7 @@ const SUBMIT_BLOCK_TOOL = {
|
|
|
111
122
|
description: "Optional: Output files to bundle. Content can be file path (auto-read) or actual content."
|
|
112
123
|
}
|
|
113
124
|
},
|
|
114
|
-
required: ["blockId", "
|
|
125
|
+
required: ["blockId", "contentHash", "consensusType", "ledger"],
|
|
115
126
|
},
|
|
116
127
|
};
|
|
117
128
|
const GET_BLOCK_TOOL = {
|
|
@@ -144,6 +155,33 @@ Use for:
|
|
|
144
155
|
required: ["blockId"],
|
|
145
156
|
},
|
|
146
157
|
};
|
|
158
|
+
const GET_LATEST_BLOCK_TOOL = {
|
|
159
|
+
name: "yamo_get_latest_block",
|
|
160
|
+
description: `Retrieves the most recently submitted YAMO block from the blockchain.
|
|
161
|
+
|
|
162
|
+
Queries BlockSubmitted events to find the block with the highest timestamp,
|
|
163
|
+
then fetches its full details including the contentHash that should be used
|
|
164
|
+
as previousBlock for the next submission.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
- blockId: Unique block identifier
|
|
168
|
+
- previousBlock: Content hash of parent block
|
|
169
|
+
- agentAddress: Ethereum address of submitter
|
|
170
|
+
- contentHash: 32-byte hash stored on-chain (use as previousBlock for next submission)
|
|
171
|
+
- timestamp: Block submission timestamp (Unix epoch)
|
|
172
|
+
- consensusType: Consensus mechanism used
|
|
173
|
+
- ledger: Distributed storage reference
|
|
174
|
+
- ipfsCID: IPFS CID if content was anchored
|
|
175
|
+
|
|
176
|
+
Use for:
|
|
177
|
+
- Automatically getting the chain tip for extending the chain
|
|
178
|
+
- Fetching the contentHash to use as previousBlock in submitBlock
|
|
179
|
+
- Discovering the latest block without knowing its ID`,
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: {},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
147
185
|
const AUDIT_BLOCK_TOOL = {
|
|
148
186
|
name: "yamo_audit_block",
|
|
149
187
|
description: `Performs cryptographic integrity audit of a block.
|
|
@@ -212,6 +250,8 @@ class YamoMcpServer {
|
|
|
212
250
|
server;
|
|
213
251
|
ipfs;
|
|
214
252
|
chain;
|
|
253
|
+
// Cache for chain continuation: latest submitted block's contentHash
|
|
254
|
+
latestContentHash = null;
|
|
215
255
|
constructor() {
|
|
216
256
|
this.server = new index_js_1.Server({ name: "yamo", version: pkg.version }, { capabilities: { tools: {} } });
|
|
217
257
|
this.ipfs = new core_1.IpfsManager();
|
|
@@ -220,29 +260,64 @@ class YamoMcpServer {
|
|
|
220
260
|
}
|
|
221
261
|
setupHandlers() {
|
|
222
262
|
this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
223
|
-
tools: [SUBMIT_BLOCK_TOOL, GET_BLOCK_TOOL, AUDIT_BLOCK_TOOL, VERIFY_BLOCK_TOOL],
|
|
263
|
+
tools: [SUBMIT_BLOCK_TOOL, GET_BLOCK_TOOL, GET_LATEST_BLOCK_TOOL, AUDIT_BLOCK_TOOL, VERIFY_BLOCK_TOOL],
|
|
224
264
|
}));
|
|
225
265
|
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
226
266
|
const { name, arguments: args } = request.params;
|
|
227
267
|
try {
|
|
228
268
|
if (name === "yamo_submit_block") {
|
|
229
269
|
const { blockId, previousBlock, contentHash, consensusType, ledger, content, files, encryptionKey } = args;
|
|
230
|
-
// Process files - auto-read if they're file paths
|
|
270
|
+
// Process files - auto-read if they're file paths (with security fix)
|
|
231
271
|
let processedFiles = files;
|
|
232
272
|
if (files && Array.isArray(files)) {
|
|
233
273
|
processedFiles = files.map((file) => {
|
|
234
274
|
// Check if content is a file path that exists
|
|
235
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
|
+
}
|
|
236
282
|
console.error(`[DEBUG] Auto-reading file from path: ${file.content}`);
|
|
237
283
|
return {
|
|
238
284
|
name: file.name,
|
|
239
|
-
content: fs_1.default.readFileSync(
|
|
285
|
+
content: fs_1.default.readFileSync(filePath, 'utf8')
|
|
240
286
|
};
|
|
241
287
|
}
|
|
242
288
|
// Otherwise use content as-is
|
|
243
289
|
return file;
|
|
244
290
|
});
|
|
245
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 chain query (for first run or if cache was cleared)
|
|
305
|
+
const latestBlock = await this.chain.getLatestBlock();
|
|
306
|
+
if (latestBlock) {
|
|
307
|
+
resolvedPreviousBlock = latestBlock.contentHash;
|
|
308
|
+
this.latestContentHash = latestBlock.contentHash; // Update cache
|
|
309
|
+
console.error(`[INFO] Using latest block's contentHash from chain: ${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
|
+
}
|
|
246
321
|
let ipfsCID = undefined;
|
|
247
322
|
if (content) {
|
|
248
323
|
ipfsCID = await this.ipfs.upload({
|
|
@@ -251,13 +326,23 @@ class YamoMcpServer {
|
|
|
251
326
|
encryptionKey
|
|
252
327
|
});
|
|
253
328
|
}
|
|
254
|
-
const
|
|
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}`);
|
|
255
334
|
return {
|
|
256
335
|
content: [{ type: "text", text: JSON.stringify({
|
|
257
336
|
success: true,
|
|
258
337
|
blockId,
|
|
259
|
-
transactionHash:
|
|
260
|
-
|
|
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()
|
|
261
346
|
}, null, 2) }],
|
|
262
347
|
};
|
|
263
348
|
}
|
|
@@ -292,6 +377,35 @@ class YamoMcpServer {
|
|
|
292
377
|
}, null, 2) }],
|
|
293
378
|
};
|
|
294
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
|
+
}
|
|
295
409
|
if (name === "yamo_audit_block") {
|
|
296
410
|
const { blockId, encryptionKey } = args;
|
|
297
411
|
// Get block from chain
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yamo/mcp-server",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "YAMO Protocol v0.4 - Model Context Protocol server for AI agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"build": "tsc",
|
|
18
18
|
"prepublishOnly": "npm run build",
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
|
-
"dev": "ts-node src/index.ts"
|
|
20
|
+
"dev": "ts-node src/index.ts",
|
|
21
|
+
"test": "node --test test/**/*.test.js",
|
|
22
|
+
"test:security": "node --test test/security.test.js"
|
|
21
23
|
},
|
|
22
24
|
"keywords": [
|
|
23
25
|
"yamo",
|
|
@@ -43,7 +45,7 @@
|
|
|
43
45
|
},
|
|
44
46
|
"dependencies": {
|
|
45
47
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
46
|
-
"@yamo/core": "^1.
|
|
48
|
+
"@yamo/core": "^1.2.7",
|
|
47
49
|
"axios": "^1.13.2",
|
|
48
50
|
"dotenv": "^17.2.3",
|
|
49
51
|
"form-data": "^4.0.5"
|