dero-mcp-server 0.1.1 → 0.1.2

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/src/server.ts DELETED
@@ -1,636 +0,0 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
- import { z } from 'zod'
3
- import { deroJsonRpc, jsonRpcEndpoint } from './rpc.js'
4
-
5
- const scRpcArgSchema = z.object({
6
- name: z.string(),
7
- datatype: z.enum(['S', 'U', 'H']),
8
- value: z.union([z.string(), z.number()]),
9
- })
10
-
11
- const hex64Schema = z
12
- .string()
13
- .regex(/^[0-9a-fA-F]{64}$/, 'Expected 64-character hex string')
14
-
15
- const deroAddressSchema = z
16
- .string()
17
- .regex(/^(dero1|deto1)[0-9a-z]+$/i, 'Expected DERO address starting with dero1 or deto1')
18
-
19
- const NAME_REGISTRY_SCID = '0000000000000000000000000000000000000000000000000000000000000001'
20
-
21
- const DERO_TOOL_NAMES = [
22
- 'dero_daemon_ping',
23
- 'dero_daemon_echo',
24
- 'dero_get_info',
25
- 'dero_get_height',
26
- 'dero_get_block_count',
27
- 'dero_get_last_block_header',
28
- 'dero_get_block',
29
- 'dero_get_block_header_by_topo_height',
30
- 'dero_get_block_header_by_hash',
31
- 'dero_get_tx_pool',
32
- 'dero_get_random_address',
33
- 'dero_get_transaction',
34
- 'dero_get_encrypted_balance',
35
- 'dero_get_sc',
36
- 'dero_get_gas_estimate',
37
- 'dero_name_to_address',
38
- 'dero_get_block_template',
39
- ] as const
40
-
41
- const DERO_RESOURCE_URIS = [
42
- 'dero://mcp/server-info',
43
- 'dero://mcp/safety-boundary',
44
- 'dero://mcp/example-flows',
45
- ] as const
46
-
47
- const DERO_PROMPT_NAMES = [
48
- 'network_health_check',
49
- 'inspect_smart_contract',
50
- 'trace_transaction',
51
- ] as const
52
-
53
- function toolText(data: unknown) {
54
- return {
55
- content: [
56
- {
57
- type: 'text' as const,
58
- text: typeof data === 'string' ? data : JSON.stringify(data, null, 2),
59
- },
60
- ],
61
- }
62
- }
63
-
64
- type StructuredToolError = {
65
- code: string
66
- hint: string
67
- retryable: boolean
68
- }
69
-
70
- function classifyToolError(error: unknown): StructuredToolError {
71
- const message = error instanceof Error ? error.message : String(error)
72
-
73
- if (message.includes('Provide either hash or height')) {
74
- return {
75
- code: 'INVALID_INPUT',
76
- hint: 'Pass exactly one of "hash" or "height".',
77
- retryable: false,
78
- }
79
- }
80
-
81
- if (message.includes('RPC error -32601')) {
82
- return {
83
- code: 'RPC_METHOD_NOT_FOUND',
84
- hint: 'Your daemon may be outdated or not a Stargate endpoint. Verify DERO_DAEMON_URL.',
85
- retryable: false,
86
- }
87
- }
88
-
89
- if (message.includes('RPC error -32602')) {
90
- return {
91
- code: 'RPC_INVALID_PARAMS',
92
- hint: 'Verify argument names and types for this tool.',
93
- retryable: false,
94
- }
95
- }
96
-
97
- const httpMatch = message.match(/HTTP (\d{3})/)
98
- if (httpMatch) {
99
- const status = Number(httpMatch[1])
100
- return {
101
- code: 'RPC_HTTP_ERROR',
102
- hint:
103
- status >= 500
104
- ? 'Daemon is reachable but errored; retry after checking node health.'
105
- : 'Check DERO_DAEMON_URL and ensure /json_rpc is accessible.',
106
- retryable: status >= 500,
107
- }
108
- }
109
-
110
- if (
111
- message.toLowerCase().includes('fetch failed') ||
112
- message.toLowerCase().includes('network') ||
113
- message.toLowerCase().includes('econnrefused') ||
114
- message.toLowerCase().includes('aborted')
115
- ) {
116
- return {
117
- code: 'RPC_UNREACHABLE',
118
- hint: 'Confirm daemon is running and reachable, then rerun `npm run doctor`.',
119
- retryable: true,
120
- }
121
- }
122
-
123
- if (message.includes('Invalid JSON from node')) {
124
- return {
125
- code: 'RPC_INVALID_RESPONSE',
126
- hint: 'Daemon returned malformed JSON. Check reverse proxies or node health.',
127
- retryable: true,
128
- }
129
- }
130
-
131
- return {
132
- code: 'TOOL_EXECUTION_ERROR',
133
- hint: 'Retry once, then inspect daemon logs and tool input values.',
134
- retryable: false,
135
- }
136
- }
137
-
138
- function toolError(tool: string, error: unknown) {
139
- const structured = classifyToolError(error)
140
- const raw = error instanceof Error ? error.message : String(error)
141
- return toolText({
142
- ok: false,
143
- tool,
144
- _meta: {
145
- error: {
146
- ...structured,
147
- raw,
148
- },
149
- },
150
- })
151
- }
152
-
153
- function withStructuredErrors<TArgs extends Record<string, unknown> | undefined>(
154
- tool: string,
155
- handler: (args: TArgs) => Promise<unknown>,
156
- ) {
157
- return async (args: TArgs) => {
158
- try {
159
- return toolText(await handler(args))
160
- } catch (error) {
161
- return toolError(tool, error)
162
- }
163
- }
164
- }
165
-
166
- export function createDeroMcpServer(daemonBaseUrl: string): McpServer {
167
- const endpoint = jsonRpcEndpoint(daemonBaseUrl)
168
- const rpc = async <T>(method: string, params?: unknown) =>
169
- deroJsonRpc<T>(endpoint, method, params)
170
-
171
- const server = new McpServer({
172
- name: 'dero-daemon-mcp',
173
- version: '0.1.0',
174
- })
175
-
176
- server.registerTool(
177
- 'dero_daemon_ping',
178
- {
179
- description:
180
- 'DERO daemon connectivity check. Calls DERO.Ping. No parameters.',
181
- },
182
- withStructuredErrors('dero_daemon_ping', async () => rpc<string>('DERO.Ping')),
183
- )
184
-
185
- server.registerTool(
186
- 'dero_daemon_echo',
187
- {
188
- description: 'Echo strings through the daemon (DERO.Echo).',
189
- inputSchema: {
190
- words: z.array(z.string()).describe('Strings to echo back'),
191
- },
192
- },
193
- withStructuredErrors('dero_daemon_echo', async ({ words }) => rpc<string>('DERO.Echo', words)),
194
- )
195
-
196
- server.registerTool(
197
- 'dero_get_info',
198
- {
199
- description:
200
- 'Get daemon / chain info: height, difficulty, version, mempool size, etc. (DERO.GetInfo).',
201
- },
202
- withStructuredErrors('dero_get_info', async () => rpc('DERO.GetInfo')),
203
- )
204
-
205
- server.registerTool(
206
- 'dero_get_height',
207
- {
208
- description: 'Get top block height and stable/topo heights (DERO.GetHeight).',
209
- },
210
- withStructuredErrors('dero_get_height', async () => rpc('DERO.GetHeight')),
211
- )
212
-
213
- server.registerTool(
214
- 'dero_get_block_count',
215
- {
216
- description: 'Total block count (DERO.GetBlockCount).',
217
- },
218
- withStructuredErrors('dero_get_block_count', async () => rpc('DERO.GetBlockCount')),
219
- )
220
-
221
- server.registerTool(
222
- 'dero_get_last_block_header',
223
- {
224
- description: 'Header of the tip block (DERO.GetLastBlockHeader).',
225
- },
226
- withStructuredErrors('dero_get_last_block_header', async () => rpc('DERO.GetLastBlockHeader')),
227
- )
228
-
229
- server.registerTool(
230
- 'dero_get_block',
231
- {
232
- description: 'Fetch a full block by height or hash (DERO.GetBlock). Provide one of hash or height.',
233
- inputSchema: {
234
- hash: hex64Schema
235
- .optional()
236
- .describe('64-char hex block hash'),
237
- height: z
238
- .number()
239
- .int()
240
- .nonnegative()
241
- .optional()
242
- .describe('Block height'),
243
- },
244
- },
245
- withStructuredErrors('dero_get_block', async (args) => {
246
- if (!args.hash && args.height === undefined) {
247
- throw new Error('Provide either hash or height')
248
- }
249
- const params: Record<string, unknown> = {}
250
- if (args.hash) params.hash = args.hash
251
- if (args.height !== undefined) params.height = args.height
252
- return rpc('DERO.GetBlock', params)
253
- }),
254
- )
255
-
256
- server.registerTool(
257
- 'dero_get_block_header_by_topo_height',
258
- {
259
- description: 'Block header by topological height (DERO.GetBlockHeaderByTopoHeight).',
260
- inputSchema: {
261
- topoheight: z
262
- .number()
263
- .int()
264
- .nonnegative()
265
- .describe('Topological height'),
266
- },
267
- },
268
- withStructuredErrors('dero_get_block_header_by_topo_height', async ({ topoheight }) =>
269
- rpc('DERO.GetBlockHeaderByTopoHeight', { topoheight })),
270
- )
271
-
272
- server.registerTool(
273
- 'dero_get_block_header_by_hash',
274
- {
275
- description: 'Block header by hash (DERO.GetBlockHeaderByHash).',
276
- inputSchema: {
277
- hash: hex64Schema.describe('Block top hash (hex)'),
278
- },
279
- },
280
- withStructuredErrors('dero_get_block_header_by_hash', async ({ hash }) =>
281
- rpc('DERO.GetBlockHeaderByHash', { hash })),
282
- )
283
-
284
- server.registerTool(
285
- 'dero_get_tx_pool',
286
- {
287
- description: 'Pending mempool transaction hashes (DERO.GetTxPool).',
288
- },
289
- withStructuredErrors('dero_get_tx_pool', async () => rpc('DERO.GetTxPool')),
290
- )
291
-
292
- server.registerTool(
293
- 'dero_get_random_address',
294
- {
295
- description:
296
- 'Random registered addresses from chain (for ring construction); optional asset scid (DERO.GetRandomAddress).',
297
- inputSchema: {
298
- scid: hex64Schema
299
- .optional()
300
- .describe('Optional asset smart-contract id (hex)'),
301
- },
302
- },
303
- withStructuredErrors('dero_get_random_address', async (args) =>
304
- rpc(
305
- 'DERO.GetRandomAddress',
306
- args.scid != null ? { scid: args.scid } : undefined,
307
- )),
308
- )
309
-
310
- server.registerTool(
311
- 'dero_get_transaction',
312
- {
313
- description: 'Fetch transactions by tx hashes (DERO.GetTransaction).',
314
- inputSchema: {
315
- txs_hashes: z
316
- .array(hex64Schema)
317
- .min(1)
318
- .describe('List of transaction hashes (hex)'),
319
- decode_as_json: z
320
- .number()
321
- .int()
322
- .optional()
323
- .describe('Optional: decode each tx as JSON when non-zero'),
324
- },
325
- },
326
- withStructuredErrors('dero_get_transaction', async ({ txs_hashes, decode_as_json }) => {
327
- const params: Record<string, unknown> = { txs_hashes }
328
- if (decode_as_json !== undefined) params.decode_as_json = decode_as_json
329
- return rpc('DERO.GetTransaction', params)
330
- }),
331
- )
332
-
333
- server.registerTool(
334
- 'dero_get_encrypted_balance',
335
- {
336
- description:
337
- 'Encrypted balance blob for an address at a topo height (DERO.GetEncryptedBalance). Not cleartext balance.',
338
- inputSchema: {
339
- address: deroAddressSchema.describe('DERO address (dero1… or deto1…)'),
340
- topoheight: z
341
- .number()
342
- .int()
343
- .describe('Use -1 for latest chain tip'),
344
- scid: hex64Schema.optional().describe('Asset SCID hex; omit for native DERO'),
345
- },
346
- },
347
- withStructuredErrors('dero_get_encrypted_balance', async ({ address, topoheight, scid }) => {
348
- const params: Record<string, unknown> = { address, topoheight }
349
- if (scid) params.scid = scid
350
- return rpc('DERO.GetEncryptedBalance', params)
351
- }),
352
- )
353
-
354
- server.registerTool(
355
- 'dero_get_sc',
356
- {
357
- description:
358
- 'Read smart contract code and/or variables by SCID (DERO.GetSC).',
359
- inputSchema: {
360
- scid: hex64Schema.describe('64-char hex Smart Contract ID'),
361
- code: z
362
- .boolean()
363
- .optional()
364
- .describe('Include contract source (default true)'),
365
- variables: z
366
- .boolean()
367
- .optional()
368
- .describe('Include stored variables (default true)'),
369
- topoheight: z
370
- .number()
371
- .int()
372
- .optional()
373
- .describe('Topo height; omit or use -1 for latest'),
374
- },
375
- },
376
- withStructuredErrors('dero_get_sc', async ({ scid, code, variables, topoheight }) => {
377
- const params: Record<string, unknown> = {
378
- scid,
379
- code: code ?? true,
380
- variables: variables ?? true,
381
- }
382
- if (topoheight !== undefined) params.topoheight = topoheight
383
- return rpc('DERO.GetSC', params)
384
- }),
385
- )
386
-
387
- server.registerTool(
388
- 'dero_get_gas_estimate',
389
- {
390
- description:
391
- 'Estimate gas (compute + storage) for transfers, deploy, or SC call (DERO.GetGasEstimate).',
392
- inputSchema: {
393
- transfers: z
394
- .array(z.record(z.unknown()))
395
- .optional()
396
- .describe('Optional transfer list'),
397
- sc: z.string().optional().describe('SC source to deploy'),
398
- sc_rpc: z
399
- .array(scRpcArgSchema)
400
- .optional()
401
- .describe('SC invocation arguments (entrypoint, SC_ID, etc.)'),
402
- signer: z
403
- .string()
404
- .optional()
405
- .describe('Signer address used for estimation'),
406
- },
407
- },
408
- withStructuredErrors('dero_get_gas_estimate', async (args) => {
409
- const params: Record<string, unknown> = {}
410
- if (args.transfers) params.transfers = args.transfers
411
- if (args.sc) params.sc = args.sc
412
- if (args.sc_rpc) params.sc_rpc = args.sc_rpc
413
- if (args.signer) params.signer = args.signer
414
- return rpc('DERO.GetGasEstimate', params)
415
- }),
416
- )
417
-
418
- server.registerTool(
419
- 'dero_name_to_address',
420
- {
421
- description: 'Resolve a DERO on-chain name to address (DERO.NameToAddress).',
422
- inputSchema: {
423
- name: z.string().min(1).describe('Registered name'),
424
- topoheight: z
425
- .number()
426
- .int()
427
- .describe('Use -1 for latest'),
428
- },
429
- },
430
- withStructuredErrors('dero_name_to_address', async ({ name, topoheight }) =>
431
- rpc('DERO.NameToAddress', { name, topoheight })),
432
- )
433
-
434
- server.registerTool(
435
- 'dero_get_block_template',
436
- {
437
- description:
438
- 'Mining: get block template for a miner address (DERO.GetBlockTemplate).',
439
- inputSchema: {
440
- wallet_address: deroAddressSchema.describe('Miner payout DERO address'),
441
- block: z
442
- .boolean()
443
- .optional()
444
- .describe('Include block blob'),
445
- miner: z.string().optional().describe('Optional miner id / label'),
446
- },
447
- },
448
- withStructuredErrors('dero_get_block_template', async ({ wallet_address, block, miner }) => {
449
- const params: Record<string, unknown> = { wallet_address }
450
- if (block !== undefined) params.block = block
451
- if (miner) params.miner = miner
452
- return rpc('DERO.GetBlockTemplate', params)
453
- }),
454
- )
455
-
456
- server.registerResource(
457
- 'dero_mcp_server_info',
458
- 'dero://mcp/server-info',
459
- {
460
- description: 'Server metadata, tool list, resource list, and prompt names.',
461
- mimeType: 'application/json',
462
- },
463
- async (uri) => ({
464
- contents: [
465
- {
466
- uri: uri.toString(),
467
- mimeType: 'application/json',
468
- text: JSON.stringify(
469
- {
470
- name: 'dero-daemon-mcp',
471
- version: '0.1.0',
472
- mode: 'read-only',
473
- endpoint: endpoint,
474
- tools: DERO_TOOL_NAMES,
475
- resources: DERO_RESOURCE_URIS,
476
- prompts: DERO_PROMPT_NAMES,
477
- },
478
- null,
479
- 2,
480
- ),
481
- },
482
- ],
483
- }),
484
- )
485
-
486
- server.registerResource(
487
- 'dero_mcp_safety_boundary',
488
- 'dero://mcp/safety-boundary',
489
- {
490
- description: 'Explicit read-only safety boundaries and escalation guidance for write actions.',
491
- mimeType: 'application/json',
492
- },
493
- async (uri) => ({
494
- contents: [
495
- {
496
- uri: uri.toString(),
497
- mimeType: 'application/json',
498
- text: JSON.stringify(
499
- {
500
- read_only: true,
501
- excluded_methods: [
502
- 'transfer',
503
- 'scinvoke',
504
- 'DERO.SendRawTransaction',
505
- 'DERO.SubmitBlock',
506
- ],
507
- reasoning: 'These methods can move funds or mutate chain state.',
508
- write_path: [
509
- 'Use wallet RPC tooling (curl/XSWD/Engram) for writes.',
510
- 'Use dero-mcp-server for live chain reads and analysis.',
511
- ],
512
- },
513
- null,
514
- 2,
515
- ),
516
- },
517
- ],
518
- }),
519
- )
520
-
521
- server.registerResource(
522
- 'dero_mcp_example_flows',
523
- 'dero://mcp/example-flows',
524
- {
525
- description: 'Compact agent flow recipes for common DERO investigations.',
526
- mimeType: 'text/markdown',
527
- },
528
- async (uri) => ({
529
- contents: [
530
- {
531
- uri: uri.toString(),
532
- mimeType: 'text/markdown',
533
- text: [
534
- '# DERO MCP Example Flows',
535
- '',
536
- '- Network health: `dero_daemon_ping` -> `dero_get_info` -> `dero_get_height`',
537
- `- Inspect SC state: \`dero_get_sc\` with SCID (name registry: \`${NAME_REGISTRY_SCID}\`)`,
538
- '- Trace transaction: `dero_get_transaction` with `decode_as_json: 1`',
539
- '- Read-only boundary: no wallet writes or raw tx submission',
540
- ].join('\n'),
541
- },
542
- ],
543
- }),
544
- )
545
-
546
- server.registerPrompt(
547
- 'network_health_check',
548
- {
549
- description: 'Guide the model through a DERO daemon sync and health check sequence.',
550
- argsSchema: {
551
- reference_topoheight: z
552
- .number()
553
- .int()
554
- .positive()
555
- .optional(),
556
- },
557
- },
558
- async ({ reference_topoheight }) => ({
559
- description: 'Prompt for sync health investigation.',
560
- messages: [
561
- {
562
- role: 'user',
563
- content: {
564
- type: 'text',
565
- text: [
566
- 'Check DERO daemon health using MCP tools.',
567
- '1) Call dero_daemon_ping.',
568
- '2) Call dero_get_info and dero_get_height.',
569
- '3) Report topoheight, stableheight, version, and network.',
570
- reference_topoheight
571
- ? `4) Compare topoheight against reference_topoheight=${reference_topoheight}.`
572
- : '4) If no reference topoheight is provided, state that external comparison is still needed for final sync confidence.',
573
- ].join('\n'),
574
- },
575
- },
576
- ],
577
- }),
578
- )
579
-
580
- server.registerPrompt(
581
- 'inspect_smart_contract',
582
- {
583
- description: 'Inspect contract code/variables and explain likely state model.',
584
- argsSchema: {
585
- scid: hex64Schema,
586
- },
587
- },
588
- async ({ scid }) => ({
589
- description: 'Prompt for smart contract inspection.',
590
- messages: [
591
- {
592
- role: 'user',
593
- content: {
594
- type: 'text',
595
- text: [
596
- `Inspect DERO smart contract ${scid}.`,
597
- '1) Call dero_get_sc with variables=true and code=true.',
598
- '2) Summarize key stringkeys and balances.',
599
- '3) Explain likely data model and any assumptions.',
600
- '4) Include topoheight context from response.',
601
- ].join('\n'),
602
- },
603
- },
604
- ],
605
- }),
606
- )
607
-
608
- server.registerPrompt(
609
- 'trace_transaction',
610
- {
611
- description: 'Trace one transaction and summarize confirmation + SC activity.',
612
- argsSchema: {
613
- tx_hash: hex64Schema,
614
- },
615
- },
616
- async ({ tx_hash }) => ({
617
- description: 'Prompt for transaction tracing.',
618
- messages: [
619
- {
620
- role: 'user',
621
- content: {
622
- type: 'text',
623
- text: [
624
- `Trace DERO transaction ${tx_hash}.`,
625
- '1) Call dero_get_transaction with txs_hashes=[tx_hash] and decode_as_json=1.',
626
- '2) Summarize confirmation status, block height, transfers, and SC invokes.',
627
- '3) If not confirmed, mention mempool status uncertainty and next check timing.',
628
- ].join('\n'),
629
- },
630
- },
631
- ],
632
- }),
633
- )
634
-
635
- return server
636
- }
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "outDir": "dist",
7
- "rootDir": "src",
8
- "strict": true,
9
- "skipLibCheck": true,
10
- "declaration": true,
11
- "declarationMap": true,
12
- "sourceMap": true,
13
- "esModuleInterop": true
14
- },
15
- "include": ["src/**/*.ts"]
16
- }