@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.
Files changed (2) hide show
  1. package/dist/index.js +126 -12
  2. 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: For genesis block use: 0x0000000000000000000000000000000000000000000000000000000000000000
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. Content and files are uploaded to IPFS (if provided)
65
- 2. Block is submitted to smart contract with hash reference
66
- 3. Returns transaction hash and IPFS CID`,
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. Genesis: 0x0000...0000"
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", "previousBlock", "contentHash", "consensusType", "ledger"],
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(file.content, 'utf8')
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 txHash = await this.chain.submitBlock(blockId, previousBlock, contentHash, consensusType, ledger, ipfsCID);
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: txHash,
260
- ipfsCID: ipfsCID || null
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.1",
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.1.0",
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"