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.
@@ -1,257 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- /**
3
- * DERO MCP Server — Flow Test
4
- *
5
- * Tests the daemon RPC calls that the MCP tools wrap.
6
- * Run this against a live daemon (local or remote) to verify connectivity
7
- * and that the RPC methods return expected data shapes.
8
- *
9
- * Usage:
10
- * npx tsx scripts/flow-test.ts
11
- * npx tsx scripts/flow-test.ts http://127.0.0.1:10102
12
- * DERO_DAEMON_URL=http://... npx tsx scripts/flow-test.ts
13
- */
14
-
15
- const DEFAULT_URL = "http://82.65.143.182:10102";
16
-
17
- type FlowStatus = "pass" | "fail" | "skip";
18
- type FlowResult = {
19
- id: string;
20
- name: string;
21
- status: FlowStatus;
22
- message?: string;
23
- durationMs: number;
24
- };
25
-
26
- async function deroRpc<T = unknown>(
27
- endpoint: string,
28
- method: string,
29
- params?: unknown
30
- ): Promise<T> {
31
- const res = await fetch(`${endpoint}/json_rpc`, {
32
- method: "POST",
33
- headers: { "Content-Type": "application/json" },
34
- body: JSON.stringify({
35
- jsonrpc: "2.0",
36
- id: "1",
37
- method,
38
- params,
39
- }),
40
- });
41
-
42
- if (!res.ok) {
43
- throw new Error(`HTTP ${res.status} from daemon`);
44
- }
45
-
46
- const data = await res.json();
47
- if (data.error) {
48
- throw new Error(`RPC error: ${JSON.stringify(data.error)}`);
49
- }
50
- return data.result;
51
- }
52
-
53
- function assert(condition: unknown, message: string): void {
54
- if (!condition) throw new Error(message);
55
- }
56
-
57
- async function runFlow(
58
- id: string,
59
- name: string,
60
- fn: () => Promise<void>
61
- ): Promise<FlowResult> {
62
- const start = performance.now();
63
- try {
64
- await fn();
65
- return { id, name, status: "pass", durationMs: Math.round(performance.now() - start) };
66
- } catch (error) {
67
- return {
68
- id,
69
- name,
70
- status: "fail",
71
- message: error instanceof Error ? error.message : String(error),
72
- durationMs: Math.round(performance.now() - start),
73
- };
74
- }
75
- }
76
-
77
- async function runAllFlows(daemonUrl: string): Promise<FlowResult[]> {
78
- const results: FlowResult[] = [];
79
-
80
- // Flow 1: Ping
81
- results.push(
82
- await runFlow("ping", "DERO.Ping — daemon reachable", async () => {
83
- const result = await deroRpc<string>(daemonUrl, "DERO.Ping");
84
- assert(result === "Pong " || result === "Pong", `Expected 'Pong', got '${result}'`);
85
- })
86
- );
87
-
88
- // Flow 2: Echo
89
- results.push(
90
- await runFlow("echo", "DERO.Echo — roundtrip strings", async () => {
91
- const words = ["hello", "dero", "mcp"];
92
- const result = await deroRpc<string>(daemonUrl, "DERO.Echo", words);
93
- assert(typeof result === "string", "Expected string response");
94
- assert(result.includes("DERO") || words.some(w => result.includes(w)), "Echo should contain input or DERO");
95
- })
96
- );
97
-
98
- // Flow 3: GetInfo
99
- let topoheight: number | undefined;
100
- results.push(
101
- await runFlow("get-info", "DERO.GetInfo — chain metadata", async () => {
102
- const result = await deroRpc<{
103
- topoheight?: number;
104
- height?: number;
105
- network?: string;
106
- version?: string;
107
- }>(daemonUrl, "DERO.GetInfo");
108
- assert(typeof result.topoheight === "number", "Missing topoheight");
109
- assert(typeof result.height === "number", "Missing height");
110
- topoheight = result.topoheight;
111
- })
112
- );
113
-
114
- // Flow 4: GetHeight
115
- results.push(
116
- await runFlow("get-height", "DERO.GetHeight — block heights", async () => {
117
- const result = await deroRpc<{
118
- height?: number;
119
- stableheight?: number;
120
- topoheight?: number;
121
- }>(daemonUrl, "DERO.GetHeight");
122
- assert(typeof result.height === "number", "Missing height");
123
- assert(typeof result.topoheight === "number", "Missing topoheight");
124
- })
125
- );
126
-
127
- // Flow 5: GetBlockCount
128
- results.push(
129
- await runFlow("get-block-count", "DERO.GetBlockCount — total blocks", async () => {
130
- const result = await deroRpc<{ count?: number }>(daemonUrl, "DERO.GetBlockCount");
131
- assert(typeof result.count === "number", "Missing count");
132
- assert(result.count > 0, "Block count should be > 0");
133
- })
134
- );
135
-
136
- // Flow 6: GetLastBlockHeader
137
- results.push(
138
- await runFlow("get-last-block-header", "DERO.GetLastBlockHeader — tip block", async () => {
139
- const result = await deroRpc<{ block_header?: { height?: number; hash?: string } }>(
140
- daemonUrl,
141
- "DERO.GetLastBlockHeader"
142
- );
143
- assert(result.block_header, "Missing block_header");
144
- assert(typeof result.block_header.height === "number", "Missing height in header");
145
- })
146
- );
147
-
148
- // Flow 7: GetBlock by height (use topoheight - 10 for safety)
149
- results.push(
150
- await runFlow("get-block-by-height", "DERO.GetBlock — fetch by height", async () => {
151
- const testHeight = Math.max(1, (topoheight ?? 100) - 10);
152
- const result = await deroRpc<{ block_header?: unknown }>(daemonUrl, "DERO.GetBlock", {
153
- height: testHeight,
154
- });
155
- assert(result.block_header, "Missing block_header in response");
156
- })
157
- );
158
-
159
- // Flow 8: GetTxPool (may be empty, just check shape)
160
- results.push(
161
- await runFlow("get-tx-pool", "DERO.GetTxPool — mempool check", async () => {
162
- const result = await deroRpc<{ tx_hashes?: string[] }>(daemonUrl, "DERO.GetTxPool");
163
- // tx_hashes may be null/undefined if empty, which is fine
164
- assert(
165
- result.tx_hashes === undefined || result.tx_hashes === null || Array.isArray(result.tx_hashes),
166
- "tx_hashes should be array or null"
167
- );
168
- })
169
- );
170
-
171
- // Flow 9: NameToAddress (test known name "dero" — may not exist on all networks)
172
- results.push(
173
- await runFlow("name-to-address", "DERO.NameToAddress — resolve 'dero'", async () => {
174
- try {
175
- const result = await deroRpc<{ address?: string; name?: string }>(
176
- daemonUrl,
177
- "DERO.NameToAddress",
178
- { name: "dero", topoheight: -1 }
179
- );
180
- // Name may not be registered, that's okay
181
- if (result.address) {
182
- assert(result.address.startsWith("dero") || result.address.startsWith("deto"), "Invalid address format");
183
- }
184
- } catch (e) {
185
- // Name not found is acceptable
186
- if (String(e).includes("NOT FOUND") || String(e).includes("not found")) {
187
- return;
188
- }
189
- throw e;
190
- }
191
- })
192
- );
193
-
194
- // Flow 10: GetSC — test with the name registry SCID
195
- const NAME_REGISTRY_SCID = "0000000000000000000000000000000000000000000000000000000000000001";
196
- results.push(
197
- await runFlow("get-sc", "DERO.GetSC — name registry contract", async () => {
198
- const result = await deroRpc<{ code?: string; balances?: unknown }>(
199
- daemonUrl,
200
- "DERO.GetSC",
201
- { scid: NAME_REGISTRY_SCID, code: true, variables: false }
202
- );
203
- assert(result.code, "Missing contract code");
204
- assert(result.code.includes("Function"), "Code should contain Function keyword");
205
- })
206
- );
207
-
208
- return results;
209
- }
210
-
211
- function formatReport(results: FlowResult[]): string {
212
- const lines: string[] = [
213
- "",
214
- "DERO MCP Flow Test Results",
215
- "==========================",
216
- "",
217
- ];
218
-
219
- const passed = results.filter((r) => r.status === "pass").length;
220
- const failed = results.filter((r) => r.status === "fail").length;
221
- const skipped = results.filter((r) => r.status === "skip").length;
222
-
223
- for (const r of results) {
224
- const icon = r.status === "pass" ? "✓" : r.status === "fail" ? "✗" : "○";
225
- const status = r.status.toUpperCase().padEnd(4);
226
- lines.push(`${icon} ${status} ${r.name} (${r.durationMs}ms)`);
227
- if (r.message) {
228
- lines.push(` ${r.message}`);
229
- }
230
- }
231
-
232
- lines.push("");
233
- lines.push(`Summary: ${passed} passed, ${failed} failed, ${skipped} skipped`);
234
- lines.push("");
235
-
236
- return lines.join("\n");
237
- }
238
-
239
- async function main() {
240
- const daemonUrl = (process.argv[2] || process.env.DERO_DAEMON_URL || DEFAULT_URL).replace(/\/$/, "");
241
-
242
- console.log(`Testing daemon at: ${daemonUrl}`);
243
- console.log("");
244
-
245
- try {
246
- const results = await runAllFlows(daemonUrl);
247
- console.log(formatReport(results));
248
-
249
- const failed = results.filter((r) => r.status === "fail").length;
250
- process.exit(failed > 0 ? 1 : 0);
251
- } catch (error) {
252
- console.error("Fatal error:", error instanceof Error ? error.message : error);
253
- process.exit(1);
254
- }
255
- }
256
-
257
- main();
@@ -1,168 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- /**
3
- * Lightweight MCP smoke probes for local stdio server contract checks.
4
- *
5
- * Verifies:
6
- * - tools/list count + name parity
7
- * - resources/list count + URI parity
8
- * - prompts/list count + name parity
9
- * - prompts/get returns usable messages
10
- * - structured tool error payload shape on execution failure
11
- */
12
-
13
- import { Client } from '@modelcontextprotocol/sdk/client/index.js'
14
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
15
-
16
- const DEFAULT_DAEMON_URL = 'http://82.65.143.182:10102'
17
- const NAME_REGISTRY_SCID = '0000000000000000000000000000000000000000000000000000000000000001'
18
-
19
- const EXPECTED_TOOLS = [
20
- 'dero_daemon_ping',
21
- 'dero_daemon_echo',
22
- 'dero_get_info',
23
- 'dero_get_height',
24
- 'dero_get_block_count',
25
- 'dero_get_last_block_header',
26
- 'dero_get_block',
27
- 'dero_get_block_header_by_topo_height',
28
- 'dero_get_block_header_by_hash',
29
- 'dero_get_tx_pool',
30
- 'dero_get_random_address',
31
- 'dero_get_transaction',
32
- 'dero_get_encrypted_balance',
33
- 'dero_get_sc',
34
- 'dero_get_gas_estimate',
35
- 'dero_name_to_address',
36
- 'dero_get_block_template',
37
- ] as const
38
-
39
- const EXPECTED_RESOURCES = [
40
- 'dero://mcp/server-info',
41
- 'dero://mcp/safety-boundary',
42
- 'dero://mcp/example-flows',
43
- ] as const
44
-
45
- const EXPECTED_PROMPTS = [
46
- 'network_health_check',
47
- 'inspect_smart_contract',
48
- 'trace_transaction',
49
- ] as const
50
-
51
- function parseArgs(argv: string[]) {
52
- let daemonUrl = process.env.DERO_DAEMON_URL ?? DEFAULT_DAEMON_URL
53
- for (let i = 0; i < argv.length; i++) {
54
- const arg = argv[i]
55
- if ((arg === '--daemon-url' || arg === '--url') && argv[i + 1]) {
56
- daemonUrl = argv[++i]
57
- } else if (arg.startsWith('--daemon-url=')) {
58
- daemonUrl = arg.slice('--daemon-url='.length)
59
- } else if (arg.startsWith('--url=')) {
60
- daemonUrl = arg.slice('--url='.length)
61
- }
62
- }
63
- return daemonUrl.replace(/\/$/, '')
64
- }
65
-
66
- function assertSortedEqual(actual: string[], expected: readonly string[], label: string) {
67
- const a = [...actual].sort()
68
- const e = [...expected].sort()
69
- if (a.length !== e.length) {
70
- throw new Error(`${label}: expected ${e.length}, got ${a.length}`)
71
- }
72
- for (let i = 0; i < e.length; i++) {
73
- if (a[i] !== e[i]) {
74
- throw new Error(`${label} mismatch at ${i}: expected ${e[i]}, got ${a[i]}`)
75
- }
76
- }
77
- }
78
-
79
- function parseFirstTextJson(result: { content: Array<{ type: string; text?: string }> }): unknown {
80
- const textEntry = result.content.find((c) => c.type === 'text' && typeof c.text === 'string')
81
- if (!textEntry?.text) {
82
- throw new Error('Tool result missing text content')
83
- }
84
- try {
85
- return JSON.parse(textEntry.text)
86
- } catch {
87
- throw new Error('Tool text content is not valid JSON')
88
- }
89
- }
90
-
91
- async function main() {
92
- const daemonUrl = parseArgs(process.argv.slice(2))
93
- console.log(`[smoke:mcp] daemon=${daemonUrl}`)
94
- console.log('================================')
95
-
96
- const transport = new StdioClientTransport({
97
- command: 'node',
98
- args: ['dist/index.js'],
99
- env: {
100
- ...process.env,
101
- DERO_DAEMON_URL: daemonUrl,
102
- } as Record<string, string>,
103
- })
104
-
105
- const client = new Client({
106
- name: 'dero-mcp-smoke-probes',
107
- version: '1.0.0',
108
- })
109
-
110
- try {
111
- await client.connect(transport)
112
-
113
- const tools = await client.listTools()
114
- const toolNames = tools.tools.map((t) => t.name)
115
- assertSortedEqual(toolNames, EXPECTED_TOOLS, 'tools/list')
116
- console.log(`OK tools/list ${toolNames.length} tools`)
117
-
118
- const resources = await client.listResources()
119
- const resourceUris = resources.resources.map((r) => r.uri)
120
- assertSortedEqual(resourceUris, EXPECTED_RESOURCES, 'resources/list')
121
- console.log(`OK resources/list ${resourceUris.length} resources`)
122
-
123
- const prompts = await client.listPrompts()
124
- const promptNames = prompts.prompts.map((p) => p.name)
125
- assertSortedEqual(promptNames, EXPECTED_PROMPTS, 'prompts/list')
126
- console.log(`OK prompts/list ${promptNames.length} prompts`)
127
-
128
- const prompt = await client.getPrompt({
129
- name: 'inspect_smart_contract',
130
- arguments: { scid: NAME_REGISTRY_SCID },
131
- })
132
- if (!prompt.messages?.length) {
133
- throw new Error('prompts/get returned zero messages')
134
- }
135
- console.log('OK prompts/get inspect_smart_contract')
136
-
137
- const structuredErrorProbe = await client.callTool({
138
- name: 'dero_get_block',
139
- arguments: {},
140
- })
141
- const errorPayload = parseFirstTextJson(structuredErrorProbe as { content: Array<{ type: string; text?: string }> }) as {
142
- ok?: boolean
143
- _meta?: { error?: { code?: string; hint?: string; retryable?: boolean } }
144
- }
145
- if (
146
- errorPayload.ok !== false ||
147
- !errorPayload._meta?.error?.code ||
148
- typeof errorPayload._meta.error.hint !== 'string' ||
149
- typeof errorPayload._meta.error.retryable !== 'boolean'
150
- ) {
151
- throw new Error('structured error probe did not return expected _meta.error shape')
152
- }
153
- console.log('OK tools/call structured _meta.error probe')
154
-
155
- console.log('')
156
- console.log('All MCP smoke probes passed.')
157
- process.exit(0)
158
- } catch (error) {
159
- console.error('')
160
- console.error('[smoke:mcp] FAIL:', error instanceof Error ? error.message : error)
161
- process.exit(1)
162
- } finally {
163
- await client.close()
164
- await transport.close()
165
- }
166
- }
167
-
168
- main()
package/server.json DELETED
@@ -1,23 +0,0 @@
1
- {
2
- "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
- "name": "io.github.DHEBP/dero-mcp-server",
4
- "title": "DERO MCP Server",
5
- "description": "Read-only DERO daemon MCP tools for chain inspection and analysis.",
6
- "version": "0.1.1",
7
- "websiteUrl": "https://derod.org",
8
- "repository": {
9
- "url": "https://github.com/DHEBP/dero-mcp-server",
10
- "source": "github"
11
- },
12
- "packages": [
13
- {
14
- "registryType": "npm",
15
- "registryBaseUrl": "https://registry.npmjs.org",
16
- "identifier": "dero-mcp-server",
17
- "version": "0.1.1",
18
- "transport": {
19
- "type": "stdio"
20
- }
21
- }
22
- ]
23
- }
package/src/index.ts DELETED
@@ -1,30 +0,0 @@
1
- #!/usr/bin/env node
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
- import { createDeroMcpServer } from './server.js'
4
-
5
- /** Default public mainnet JSON-RPC (override with DERO_DAEMON_URL). */
6
- const DEFAULT_DAEMON_BASE = 'http://82.65.143.182:10102'
7
-
8
- function daemonUrlFromEnv(): string {
9
- const fromEnv = process.env.DERO_DAEMON_URL?.trim()
10
- if (fromEnv) return fromEnv.replace(/\/json_rpc\/?$/, '')
11
- return DEFAULT_DAEMON_BASE
12
- }
13
-
14
- async function main() {
15
- const base = daemonUrlFromEnv()
16
- const server = createDeroMcpServer(base)
17
-
18
- const transport = new StdioServerTransport()
19
-
20
- process.stderr.write(
21
- `[dero-mcp-server] DERO_DAEMON_URL base: ${base} (JSON-RPC at ${base.replace(/\/$/, '')}/json_rpc)\n`,
22
- )
23
-
24
- await server.connect(transport)
25
- }
26
-
27
- main().catch((err) => {
28
- process.stderr.write(`[dero-mcp-server] fatal: ${err instanceof Error ? err.message : String(err)}\n`)
29
- process.exit(1)
30
- })
package/src/rpc.ts DELETED
@@ -1,60 +0,0 @@
1
- const DEFAULT_TIMEOUT_MS = 45_000
2
-
3
- export type JsonRpcResponse<T = unknown> = {
4
- jsonrpc: '2.0'
5
- id: string | number
6
- result?: T
7
- error?: { code: number; message: string; data?: unknown }
8
- }
9
-
10
- /**
11
- * POST JSON-RPC 2.0 to a DERO daemon or wallet endpoint (…/json_rpc).
12
- */
13
- export async function deroJsonRpc<T = unknown>(
14
- jsonRpcUrl: string,
15
- method: string,
16
- params?: unknown,
17
- options?: { timeoutMs?: number },
18
- ): Promise<T> {
19
- const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS
20
- const body: Record<string, unknown> = {
21
- jsonrpc: '2.0',
22
- id: 'dero-mcp',
23
- method,
24
- }
25
- if (params !== undefined) body.params = params
26
-
27
- const controller = new AbortController()
28
- const timer = setTimeout(() => controller.abort(), timeoutMs)
29
- try {
30
- const res = await fetch(jsonRpcUrl, {
31
- method: 'POST',
32
- headers: { 'content-type': 'application/json' },
33
- body: JSON.stringify(body),
34
- signal: controller.signal,
35
- })
36
- const text = await res.text()
37
- if (!res.ok) {
38
- throw new Error(`HTTP ${res.status}: ${text.slice(0, 500)}`)
39
- }
40
- let json: JsonRpcResponse<T>
41
- try {
42
- json = JSON.parse(text) as JsonRpcResponse<T>
43
- } catch {
44
- throw new Error(`Invalid JSON from node: ${text.slice(0, 200)}`)
45
- }
46
- if (json.error) {
47
- throw new Error(
48
- `RPC error ${json.error.code}: ${json.error.message}${json.error.data != null ? ` ${JSON.stringify(json.error.data)}` : ''}`,
49
- )
50
- }
51
- return json.result as T
52
- } finally {
53
- clearTimeout(timer)
54
- }
55
- }
56
-
57
- export function jsonRpcEndpoint(baseUrl: string): string {
58
- const trimmed = baseUrl.replace(/\/$/, '')
59
- return trimmed.endsWith('/json_rpc') ? trimmed : `${trimmed}/json_rpc`
60
- }