dero-mcp-server 0.1.1 → 0.2.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.
Files changed (58) hide show
  1. package/README.md +79 -6
  2. package/data/docs-index.json +5702 -0
  3. package/dist/citations.d.ts +70 -0
  4. package/dist/citations.d.ts.map +1 -0
  5. package/dist/citations.js +162 -0
  6. package/dist/citations.js.map +1 -0
  7. package/dist/composites/_shared.d.ts +119 -0
  8. package/dist/composites/_shared.d.ts.map +1 -0
  9. package/dist/composites/_shared.js +152 -0
  10. package/dist/composites/_shared.js.map +1 -0
  11. package/dist/composites/diagnose-chain-health.d.ts +64 -0
  12. package/dist/composites/diagnose-chain-health.d.ts.map +1 -0
  13. package/dist/composites/diagnose-chain-health.js +144 -0
  14. package/dist/composites/diagnose-chain-health.js.map +1 -0
  15. package/dist/composites/estimate-deploy-cost.d.ts +83 -0
  16. package/dist/composites/estimate-deploy-cost.d.ts.map +1 -0
  17. package/dist/composites/estimate-deploy-cost.js +116 -0
  18. package/dist/composites/estimate-deploy-cost.js.map +1 -0
  19. package/dist/composites/explain-smart-contract.d.ts +64 -0
  20. package/dist/composites/explain-smart-contract.d.ts.map +1 -0
  21. package/dist/composites/explain-smart-contract.js +149 -0
  22. package/dist/composites/explain-smart-contract.js.map +1 -0
  23. package/dist/composites/recommend-docs-path.d.ts +97 -0
  24. package/dist/composites/recommend-docs-path.d.ts.map +1 -0
  25. package/dist/composites/recommend-docs-path.js +149 -0
  26. package/dist/composites/recommend-docs-path.js.map +1 -0
  27. package/dist/composites/trace-transaction-with-context.d.ts +107 -0
  28. package/dist/composites/trace-transaction-with-context.d.ts.map +1 -0
  29. package/dist/composites/trace-transaction-with-context.js +217 -0
  30. package/dist/composites/trace-transaction-with-context.js.map +1 -0
  31. package/dist/docs-parse.d.ts +30 -0
  32. package/dist/docs-parse.d.ts.map +1 -0
  33. package/dist/docs-parse.js +147 -0
  34. package/dist/docs-parse.js.map +1 -0
  35. package/dist/docs.d.ts +101 -0
  36. package/dist/docs.d.ts.map +1 -0
  37. package/dist/docs.js +172 -0
  38. package/dist/docs.js.map +1 -0
  39. package/dist/server.d.ts.map +1 -1
  40. package/dist/server.js +417 -100
  41. package/dist/server.js.map +1 -1
  42. package/dist/tool-descriptions.d.ts +50 -0
  43. package/dist/tool-descriptions.d.ts.map +1 -0
  44. package/dist/tool-descriptions.js +246 -0
  45. package/dist/tool-descriptions.js.map +1 -0
  46. package/package.json +15 -3
  47. package/.github/workflows/ci.yml +0 -62
  48. package/docs/example-agent-flows.md +0 -236
  49. package/docs/mcp-agent-ready-evidence.md +0 -108
  50. package/glama.json +0 -6
  51. package/scripts/doctor.sh +0 -85
  52. package/scripts/flow-test.ts +0 -257
  53. package/scripts/mcp-smoke-probes.ts +0 -168
  54. package/server.json +0 -23
  55. package/src/index.ts +0 -30
  56. package/src/rpc.ts +0 -60
  57. package/src/server.ts +0 -636
  58. package/tsconfig.json +0 -16
package/dist/server.js CHANGED
@@ -1,6 +1,14 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
  import { deroJsonRpc, jsonRpcEndpoint } from './rpc.js';
4
+ import { DERO_DOC_PRODUCTS, getDeroDocPage, listDeroDocs, searchDeroDocs, } from './docs.js';
5
+ import { DERO_TOOL_NAMES, TOOL_DESCRIPTIONS } from './tool-descriptions.js';
6
+ import { relatedDocsFor } from './citations.js';
7
+ import { diagnoseChainHealth, diagnoseChainHealthInputSchema, } from './composites/diagnose-chain-health.js';
8
+ import { explainSmartContract, explainSmartContractInputSchema, } from './composites/explain-smart-contract.js';
9
+ import { recommendDocsPath, recommendDocsPathInputSchema, } from './composites/recommend-docs-path.js';
10
+ import { estimateDeployCost, estimateDeployCostInputSchema, } from './composites/estimate-deploy-cost.js';
11
+ import { traceTransactionWithContext, traceTransactionWithContextInputSchema, } from './composites/trace-transaction-with-context.js';
4
12
  const scRpcArgSchema = z.object({
5
13
  name: z.string(),
6
14
  datatype: z.enum(['S', 'U', 'H']),
@@ -13,35 +21,20 @@ const deroAddressSchema = z
13
21
  .string()
14
22
  .regex(/^(dero1|deto1)[0-9a-z]+$/i, 'Expected DERO address starting with dero1 or deto1');
15
23
  const NAME_REGISTRY_SCID = '0000000000000000000000000000000000000000000000000000000000000001';
16
- const DERO_TOOL_NAMES = [
17
- 'dero_daemon_ping',
18
- 'dero_daemon_echo',
19
- 'dero_get_info',
20
- 'dero_get_height',
21
- 'dero_get_block_count',
22
- 'dero_get_last_block_header',
23
- 'dero_get_block',
24
- 'dero_get_block_header_by_topo_height',
25
- 'dero_get_block_header_by_hash',
26
- 'dero_get_tx_pool',
27
- 'dero_get_random_address',
28
- 'dero_get_transaction',
29
- 'dero_get_encrypted_balance',
30
- 'dero_get_sc',
31
- 'dero_get_gas_estimate',
32
- 'dero_name_to_address',
33
- 'dero_get_block_template',
34
- ];
35
24
  const DERO_RESOURCE_URIS = [
36
25
  'dero://mcp/server-info',
37
26
  'dero://mcp/safety-boundary',
38
27
  'dero://mcp/example-flows',
28
+ 'dero://mcp/composites',
39
29
  ];
40
30
  const DERO_PROMPT_NAMES = [
41
31
  'network_health_check',
42
32
  'inspect_smart_contract',
43
33
  'trace_transaction',
34
+ 'find_dero_docs_for_intent',
35
+ 'estimate_deploy_for_contract',
44
36
  ];
37
+ const deroDocProductSchema = z.enum(DERO_DOC_PRODUCTS);
45
38
  function toolText(data) {
46
39
  return {
47
40
  content: [
@@ -61,6 +54,49 @@ function classifyToolError(error) {
61
54
  retryable: false,
62
55
  };
63
56
  }
57
+ if (message.includes('DERO docs unavailable') ||
58
+ message.includes('bundled docs index is missing')) {
59
+ return {
60
+ code: 'DOCS_UNAVAILABLE',
61
+ hint: 'Bundled docs index is missing from this install. Reinstall dero-mcp-server or set DERO_DOCS_ROOT for local dev override.',
62
+ retryable: false,
63
+ };
64
+ }
65
+ if (message.includes('DERO docs search requires a non-empty query')) {
66
+ return {
67
+ code: 'INVALID_INPUT',
68
+ hint: 'Pass a non-empty "query" string for dero_docs_search.',
69
+ retryable: false,
70
+ };
71
+ }
72
+ if (message.includes('DERO docs get page requires a non-empty slug')) {
73
+ return {
74
+ code: 'INVALID_INPUT',
75
+ hint: 'Pass a non-empty "slug" for dero_docs_get_page.',
76
+ retryable: false,
77
+ };
78
+ }
79
+ if (message.includes('Doc page not found')) {
80
+ return {
81
+ code: 'DOC_NOT_FOUND',
82
+ hint: 'Use dero_docs_search or dero_docs_list to discover valid slugs, then retry.',
83
+ retryable: false,
84
+ };
85
+ }
86
+ if (message.includes('No DERO docs matched intent')) {
87
+ return {
88
+ code: 'NO_DOCS_MATCH',
89
+ hint: 'Rephrase the intent (drop verbs, use product nouns like "TELA app" or "DVM contract"), then retry. You can also pass product_hint to bias the search.',
90
+ retryable: false,
91
+ };
92
+ }
93
+ if (message.includes('DERO transaction not found')) {
94
+ return {
95
+ code: 'TX_NOT_FOUND',
96
+ hint: 'The daemon has no record of that tx hash on this chain. Verify the hash is correct (64 hex chars), check whether you queried the right network (mainnet vs testnet), and if the tx is freshly broadcast wait a few seconds for mempool propagation and retry.',
97
+ retryable: true,
98
+ };
99
+ }
64
100
  if (message.includes('RPC error -32601')) {
65
101
  return {
66
102
  code: 'RPC_METHOD_NOT_FOUND',
@@ -75,6 +111,13 @@ function classifyToolError(error) {
75
111
  retryable: false,
76
112
  };
77
113
  }
114
+ if (message.includes('RPC error -32098')) {
115
+ return {
116
+ code: 'INVALID_INPUT',
117
+ hint: 'The DVM compiler rejected the contract source. Inspect _meta.error.raw for the exact compile error (often points at a line, symbol, or missing keyword). Common causes: missing `End Function`, missing return type (`Uint64`/`String`), unbalanced parens, or sending a function body instead of a full contract.',
118
+ retryable: false,
119
+ };
120
+ }
78
121
  const httpMatch = message.match(/HTTP (\d{3})/);
79
122
  if (httpMatch) {
80
123
  const status = Number(httpMatch[1]);
@@ -133,36 +176,69 @@ function withStructuredErrors(tool, handler) {
133
176
  }
134
177
  };
135
178
  }
179
+ /**
180
+ * MCP tool annotation hint block applied to every tool in this server.
181
+ *
182
+ * - `readOnlyHint: true` lets MCP hosts (Cursor, Claude Desktop, OpenCode)
183
+ * auto-approve calls without per-invocation confirmation.
184
+ * - `destructiveHint: false` makes the read-only promise explicit so hosts
185
+ * render a safe-call badge.
186
+ * - `idempotentHint: false` because chain state advances between calls —
187
+ * identical inputs may return different blocks/heights/tx pools.
188
+ * - `openWorldHint: false` because we hit a configured daemon endpoint only,
189
+ * not arbitrary external services.
190
+ *
191
+ * Any future wallet/write tools MUST use a different annotation block
192
+ * (`readOnlyHint: false`, `destructiveHint: true`) and remain require-approval.
193
+ */
194
+ const READ_ONLY_ANNOTATIONS = {
195
+ readOnlyHint: true,
196
+ destructiveHint: false,
197
+ idempotentHint: false,
198
+ openWorldHint: false,
199
+ };
200
+ /**
201
+ * Helper that tags a tool config with the read-only annotation block.
202
+ * Use for every primitive in this v0.1 server. Composites built on these
203
+ * primitives are also read-only and should use this same helper.
204
+ */
205
+ function readOnly(config) {
206
+ return { ...config, annotations: READ_ONLY_ANNOTATIONS };
207
+ }
136
208
  export function createDeroMcpServer(daemonBaseUrl) {
137
209
  const endpoint = jsonRpcEndpoint(daemonBaseUrl);
138
210
  const rpc = async (method, params) => deroJsonRpc(endpoint, method, params);
139
211
  const server = new McpServer({
140
212
  name: 'dero-daemon-mcp',
141
- version: '0.1.0',
213
+ version: '0.2.2',
142
214
  });
143
- server.registerTool('dero_daemon_ping', {
144
- description: 'DERO daemon connectivity check. Calls DERO.Ping. No parameters.',
145
- }, withStructuredErrors('dero_daemon_ping', async () => rpc('DERO.Ping')));
146
- server.registerTool('dero_daemon_echo', {
147
- description: 'Echo strings through the daemon (DERO.Echo).',
215
+ server.registerTool('dero_daemon_ping', readOnly({
216
+ description: TOOL_DESCRIPTIONS.dero_daemon_ping,
217
+ }), withStructuredErrors('dero_daemon_ping', async () => rpc('DERO.Ping')));
218
+ server.registerTool('dero_daemon_echo', readOnly({
219
+ description: TOOL_DESCRIPTIONS.dero_daemon_echo,
148
220
  inputSchema: {
149
221
  words: z.array(z.string()).describe('Strings to echo back'),
150
222
  },
151
- }, withStructuredErrors('dero_daemon_echo', async ({ words }) => rpc('DERO.Echo', words)));
152
- server.registerTool('dero_get_info', {
153
- description: 'Get daemon / chain info: height, difficulty, version, mempool size, etc. (DERO.GetInfo).',
154
- }, withStructuredErrors('dero_get_info', async () => rpc('DERO.GetInfo')));
155
- server.registerTool('dero_get_height', {
156
- description: 'Get top block height and stable/topo heights (DERO.GetHeight).',
157
- }, withStructuredErrors('dero_get_height', async () => rpc('DERO.GetHeight')));
158
- server.registerTool('dero_get_block_count', {
159
- description: 'Total block count (DERO.GetBlockCount).',
160
- }, withStructuredErrors('dero_get_block_count', async () => rpc('DERO.GetBlockCount')));
161
- server.registerTool('dero_get_last_block_header', {
162
- description: 'Header of the tip block (DERO.GetLastBlockHeader).',
163
- }, withStructuredErrors('dero_get_last_block_header', async () => rpc('DERO.GetLastBlockHeader')));
164
- server.registerTool('dero_get_block', {
165
- description: 'Fetch a full block by height or hash (DERO.GetBlock). Provide one of hash or height.',
223
+ }), withStructuredErrors('dero_daemon_echo', async ({ words }) => rpc('DERO.Echo', words)));
224
+ server.registerTool('dero_get_info', readOnly({
225
+ description: TOOL_DESCRIPTIONS.dero_get_info,
226
+ }), withStructuredErrors('dero_get_info', async () => {
227
+ const result = (await rpc('DERO.GetInfo')) ?? {};
228
+ const related_docs = relatedDocsFor('dero_get_info');
229
+ return { ...result, ...(related_docs ? { related_docs } : {}) };
230
+ }));
231
+ server.registerTool('dero_get_height', readOnly({
232
+ description: TOOL_DESCRIPTIONS.dero_get_height,
233
+ }), withStructuredErrors('dero_get_height', async () => rpc('DERO.GetHeight')));
234
+ server.registerTool('dero_get_block_count', readOnly({
235
+ description: TOOL_DESCRIPTIONS.dero_get_block_count,
236
+ }), withStructuredErrors('dero_get_block_count', async () => rpc('DERO.GetBlockCount')));
237
+ server.registerTool('dero_get_last_block_header', readOnly({
238
+ description: TOOL_DESCRIPTIONS.dero_get_last_block_header,
239
+ }), withStructuredErrors('dero_get_last_block_header', async () => rpc('DERO.GetLastBlockHeader')));
240
+ server.registerTool('dero_get_block', readOnly({
241
+ description: TOOL_DESCRIPTIONS.dero_get_block,
166
242
  inputSchema: {
167
243
  hash: hex64Schema
168
244
  .optional()
@@ -174,7 +250,7 @@ export function createDeroMcpServer(daemonBaseUrl) {
174
250
  .optional()
175
251
  .describe('Block height'),
176
252
  },
177
- }, withStructuredErrors('dero_get_block', async (args) => {
253
+ }), withStructuredErrors('dero_get_block', async (args) => {
178
254
  if (!args.hash && args.height === undefined) {
179
255
  throw new Error('Provide either hash or height');
180
256
  }
@@ -185,8 +261,8 @@ export function createDeroMcpServer(daemonBaseUrl) {
185
261
  params.height = args.height;
186
262
  return rpc('DERO.GetBlock', params);
187
263
  }));
188
- server.registerTool('dero_get_block_header_by_topo_height', {
189
- description: 'Block header by topological height (DERO.GetBlockHeaderByTopoHeight).',
264
+ server.registerTool('dero_get_block_header_by_topo_height', readOnly({
265
+ description: TOOL_DESCRIPTIONS.dero_get_block_header_by_topo_height,
190
266
  inputSchema: {
191
267
  topoheight: z
192
268
  .number()
@@ -194,26 +270,26 @@ export function createDeroMcpServer(daemonBaseUrl) {
194
270
  .nonnegative()
195
271
  .describe('Topological height'),
196
272
  },
197
- }, withStructuredErrors('dero_get_block_header_by_topo_height', async ({ topoheight }) => rpc('DERO.GetBlockHeaderByTopoHeight', { topoheight })));
198
- server.registerTool('dero_get_block_header_by_hash', {
199
- description: 'Block header by hash (DERO.GetBlockHeaderByHash).',
273
+ }), withStructuredErrors('dero_get_block_header_by_topo_height', async ({ topoheight }) => rpc('DERO.GetBlockHeaderByTopoHeight', { topoheight })));
274
+ server.registerTool('dero_get_block_header_by_hash', readOnly({
275
+ description: TOOL_DESCRIPTIONS.dero_get_block_header_by_hash,
200
276
  inputSchema: {
201
277
  hash: hex64Schema.describe('Block top hash (hex)'),
202
278
  },
203
- }, withStructuredErrors('dero_get_block_header_by_hash', async ({ hash }) => rpc('DERO.GetBlockHeaderByHash', { hash })));
204
- server.registerTool('dero_get_tx_pool', {
205
- description: 'Pending mempool transaction hashes (DERO.GetTxPool).',
206
- }, withStructuredErrors('dero_get_tx_pool', async () => rpc('DERO.GetTxPool')));
207
- server.registerTool('dero_get_random_address', {
208
- description: 'Random registered addresses from chain (for ring construction); optional asset scid (DERO.GetRandomAddress).',
279
+ }), withStructuredErrors('dero_get_block_header_by_hash', async ({ hash }) => rpc('DERO.GetBlockHeaderByHash', { hash })));
280
+ server.registerTool('dero_get_tx_pool', readOnly({
281
+ description: TOOL_DESCRIPTIONS.dero_get_tx_pool,
282
+ }), withStructuredErrors('dero_get_tx_pool', async () => rpc('DERO.GetTxPool')));
283
+ server.registerTool('dero_get_random_address', readOnly({
284
+ description: TOOL_DESCRIPTIONS.dero_get_random_address,
209
285
  inputSchema: {
210
286
  scid: hex64Schema
211
287
  .optional()
212
288
  .describe('Optional asset smart-contract id (hex)'),
213
289
  },
214
- }, withStructuredErrors('dero_get_random_address', async (args) => rpc('DERO.GetRandomAddress', args.scid != null ? { scid: args.scid } : undefined)));
215
- server.registerTool('dero_get_transaction', {
216
- description: 'Fetch transactions by tx hashes (DERO.GetTransaction).',
290
+ }), withStructuredErrors('dero_get_random_address', async (args) => rpc('DERO.GetRandomAddress', args.scid != null ? { scid: args.scid } : undefined)));
291
+ server.registerTool('dero_get_transaction', readOnly({
292
+ description: TOOL_DESCRIPTIONS.dero_get_transaction,
217
293
  inputSchema: {
218
294
  txs_hashes: z
219
295
  .array(hex64Schema)
@@ -225,14 +301,14 @@ export function createDeroMcpServer(daemonBaseUrl) {
225
301
  .optional()
226
302
  .describe('Optional: decode each tx as JSON when non-zero'),
227
303
  },
228
- }, withStructuredErrors('dero_get_transaction', async ({ txs_hashes, decode_as_json }) => {
304
+ }), withStructuredErrors('dero_get_transaction', async ({ txs_hashes, decode_as_json }) => {
229
305
  const params = { txs_hashes };
230
306
  if (decode_as_json !== undefined)
231
307
  params.decode_as_json = decode_as_json;
232
308
  return rpc('DERO.GetTransaction', params);
233
309
  }));
234
- server.registerTool('dero_get_encrypted_balance', {
235
- description: 'Encrypted balance blob for an address at a topo height (DERO.GetEncryptedBalance). Not cleartext balance.',
310
+ server.registerTool('dero_get_encrypted_balance', readOnly({
311
+ description: TOOL_DESCRIPTIONS.dero_get_encrypted_balance,
236
312
  inputSchema: {
237
313
  address: deroAddressSchema.describe('DERO address (dero1… or deto1…)'),
238
314
  topoheight: z
@@ -241,14 +317,14 @@ export function createDeroMcpServer(daemonBaseUrl) {
241
317
  .describe('Use -1 for latest chain tip'),
242
318
  scid: hex64Schema.optional().describe('Asset SCID hex; omit for native DERO'),
243
319
  },
244
- }, withStructuredErrors('dero_get_encrypted_balance', async ({ address, topoheight, scid }) => {
320
+ }), withStructuredErrors('dero_get_encrypted_balance', async ({ address, topoheight, scid }) => {
245
321
  const params = { address, topoheight };
246
322
  if (scid)
247
323
  params.scid = scid;
248
324
  return rpc('DERO.GetEncryptedBalance', params);
249
325
  }));
250
- server.registerTool('dero_get_sc', {
251
- description: 'Read smart contract code and/or variables by SCID (DERO.GetSC).',
326
+ server.registerTool('dero_get_sc', readOnly({
327
+ description: TOOL_DESCRIPTIONS.dero_get_sc,
252
328
  inputSchema: {
253
329
  scid: hex64Schema.describe('64-char hex Smart Contract ID'),
254
330
  code: z
@@ -265,7 +341,7 @@ export function createDeroMcpServer(daemonBaseUrl) {
265
341
  .optional()
266
342
  .describe('Topo height; omit or use -1 for latest'),
267
343
  },
268
- }, withStructuredErrors('dero_get_sc', async ({ scid, code, variables, topoheight }) => {
344
+ }), withStructuredErrors('dero_get_sc', async ({ scid, code, variables, topoheight }) => {
269
345
  const params = {
270
346
  scid,
271
347
  code: code ?? true,
@@ -273,10 +349,12 @@ export function createDeroMcpServer(daemonBaseUrl) {
273
349
  };
274
350
  if (topoheight !== undefined)
275
351
  params.topoheight = topoheight;
276
- return rpc('DERO.GetSC', params);
352
+ const result = (await rpc('DERO.GetSC', params)) ?? {};
353
+ const related_docs = relatedDocsFor('dero_get_sc');
354
+ return { ...result, ...(related_docs ? { related_docs } : {}) };
277
355
  }));
278
- server.registerTool('dero_get_gas_estimate', {
279
- description: 'Estimate gas (compute + storage) for transfers, deploy, or SC call (DERO.GetGasEstimate).',
356
+ server.registerTool('dero_get_gas_estimate', readOnly({
357
+ description: TOOL_DESCRIPTIONS.dero_get_gas_estimate,
280
358
  inputSchema: {
281
359
  transfers: z
282
360
  .array(z.record(z.unknown()))
@@ -292,7 +370,7 @@ export function createDeroMcpServer(daemonBaseUrl) {
292
370
  .optional()
293
371
  .describe('Signer address used for estimation'),
294
372
  },
295
- }, withStructuredErrors('dero_get_gas_estimate', async (args) => {
373
+ }), withStructuredErrors('dero_get_gas_estimate', async (args) => {
296
374
  const params = {};
297
375
  if (args.transfers)
298
376
  params.transfers = args.transfers;
@@ -302,10 +380,12 @@ export function createDeroMcpServer(daemonBaseUrl) {
302
380
  params.sc_rpc = args.sc_rpc;
303
381
  if (args.signer)
304
382
  params.signer = args.signer;
305
- return rpc('DERO.GetGasEstimate', params);
383
+ const result = (await rpc('DERO.GetGasEstimate', params)) ?? {};
384
+ const related_docs = relatedDocsFor('dero_get_gas_estimate');
385
+ return { ...result, ...(related_docs ? { related_docs } : {}) };
306
386
  }));
307
- server.registerTool('dero_name_to_address', {
308
- description: 'Resolve a DERO on-chain name to address (DERO.NameToAddress).',
387
+ server.registerTool('dero_name_to_address', readOnly({
388
+ description: TOOL_DESCRIPTIONS.dero_name_to_address,
309
389
  inputSchema: {
310
390
  name: z.string().min(1).describe('Registered name'),
311
391
  topoheight: z
@@ -313,9 +393,9 @@ export function createDeroMcpServer(daemonBaseUrl) {
313
393
  .int()
314
394
  .describe('Use -1 for latest'),
315
395
  },
316
- }, withStructuredErrors('dero_name_to_address', async ({ name, topoheight }) => rpc('DERO.NameToAddress', { name, topoheight })));
317
- server.registerTool('dero_get_block_template', {
318
- description: 'Mining: get block template for a miner address (DERO.GetBlockTemplate).',
396
+ }), withStructuredErrors('dero_name_to_address', async ({ name, topoheight }) => rpc('DERO.NameToAddress', { name, topoheight })));
397
+ server.registerTool('dero_get_block_template', readOnly({
398
+ description: TOOL_DESCRIPTIONS.dero_get_block_template,
319
399
  inputSchema: {
320
400
  wallet_address: deroAddressSchema.describe('Miner payout DERO address'),
321
401
  block: z
@@ -324,7 +404,7 @@ export function createDeroMcpServer(daemonBaseUrl) {
324
404
  .describe('Include block blob'),
325
405
  miner: z.string().optional().describe('Optional miner id / label'),
326
406
  },
327
- }, withStructuredErrors('dero_get_block_template', async ({ wallet_address, block, miner }) => {
407
+ }), withStructuredErrors('dero_get_block_template', async ({ wallet_address, block, miner }) => {
328
408
  const params = { wallet_address };
329
409
  if (block !== undefined)
330
410
  params.block = block;
@@ -332,6 +412,89 @@ export function createDeroMcpServer(daemonBaseUrl) {
332
412
  params.miner = miner;
333
413
  return rpc('DERO.GetBlockTemplate', params);
334
414
  }));
415
+ server.registerTool('dero_docs_search', readOnly({
416
+ description: TOOL_DESCRIPTIONS.dero_docs_search,
417
+ inputSchema: {
418
+ query: z
419
+ .string()
420
+ .min(1)
421
+ .describe('Search text (e.g., "wallet rpc", "tela deployment", "deropay webhooks")'),
422
+ product: deroDocProductSchema
423
+ .optional()
424
+ .describe('Optional docs product filter: derod | tela | hologram | deropay'),
425
+ section: z
426
+ .string()
427
+ .optional()
428
+ .describe('Optional section slug prefix (e.g., "rpc-api", "guides", "dero-pay")'),
429
+ limit: z
430
+ .number()
431
+ .int()
432
+ .min(1)
433
+ .max(25)
434
+ .optional()
435
+ .describe('Max matches (default 8, max 25)'),
436
+ },
437
+ }), withStructuredErrors('dero_docs_search', async ({ query, product, section, limit }) => searchDeroDocs({ query, product, section, limit })));
438
+ server.registerTool('dero_docs_get_page', readOnly({
439
+ description: TOOL_DESCRIPTIONS.dero_docs_get_page,
440
+ inputSchema: {
441
+ slug: z
442
+ .string()
443
+ .min(1)
444
+ .describe('Doc slug relative to pages/ (e.g., "rpc-api/daemon-rpc-api", "tutorials/first-app", "dero-pay/quick-start")'),
445
+ product: deroDocProductSchema
446
+ .optional()
447
+ .describe('Optional product scope to disambiguate duplicate slugs'),
448
+ },
449
+ }), withStructuredErrors('dero_docs_get_page', async ({ slug, product }) => getDeroDocPage({ slug, product })));
450
+ server.registerTool('dero_docs_list', readOnly({
451
+ description: TOOL_DESCRIPTIONS.dero_docs_list,
452
+ inputSchema: {
453
+ product: deroDocProductSchema
454
+ .optional()
455
+ .describe('Optional docs product filter: derod | tela | hologram | deropay'),
456
+ limit: z
457
+ .number()
458
+ .int()
459
+ .min(1)
460
+ .max(500)
461
+ .optional()
462
+ .describe('Max pages returned (default 120, max 500)'),
463
+ },
464
+ }), withStructuredErrors('dero_docs_list', async ({ product, limit }) => {
465
+ const docsIndex = await listDeroDocs(product);
466
+ const capped = Math.max(1, Math.min(limit ?? 120, 500));
467
+ return {
468
+ ...docsIndex,
469
+ returned: Math.min(capped, docsIndex.pages.length),
470
+ pages: docsIndex.pages.slice(0, capped),
471
+ };
472
+ }));
473
+ // ---------- Composite tools (Phase C) ----------
474
+ // Composites chain read-only primitives and bundled docs into
475
+ // intent-shaped responses. Each composite has a design entry in
476
+ // `docs/composites.md` that pins its input schema, internal chain,
477
+ // response shape, failure modes, and flow test ID.
478
+ server.registerTool('diagnose_chain_health', readOnly({
479
+ description: TOOL_DESCRIPTIONS.diagnose_chain_health,
480
+ inputSchema: diagnoseChainHealthInputSchema,
481
+ }), withStructuredErrors('diagnose_chain_health', async (args) => diagnoseChainHealth(rpc, args ?? {})));
482
+ server.registerTool('explain_smart_contract', readOnly({
483
+ description: TOOL_DESCRIPTIONS.explain_smart_contract,
484
+ inputSchema: explainSmartContractInputSchema,
485
+ }), withStructuredErrors('explain_smart_contract', async (args) => explainSmartContract(rpc, args)));
486
+ server.registerTool('recommend_docs_path', readOnly({
487
+ description: TOOL_DESCRIPTIONS.recommend_docs_path,
488
+ inputSchema: recommendDocsPathInputSchema,
489
+ }), withStructuredErrors('recommend_docs_path', async (args) => recommendDocsPath(args)));
490
+ server.registerTool('estimate_deploy_cost', readOnly({
491
+ description: TOOL_DESCRIPTIONS.estimate_deploy_cost,
492
+ inputSchema: estimateDeployCostInputSchema,
493
+ }), withStructuredErrors('estimate_deploy_cost', async (args) => estimateDeployCost(rpc, args)));
494
+ server.registerTool('trace_transaction_with_context', readOnly({
495
+ description: TOOL_DESCRIPTIONS.trace_transaction_with_context,
496
+ inputSchema: traceTransactionWithContextInputSchema,
497
+ }), withStructuredErrors('trace_transaction_with_context', async (args) => traceTransactionWithContext(rpc, args)));
335
498
  server.registerResource('dero_mcp_server_info', 'dero://mcp/server-info', {
336
499
  description: 'Server metadata, tool list, resource list, and prompt names.',
337
500
  mimeType: 'application/json',
@@ -342,9 +505,12 @@ export function createDeroMcpServer(daemonBaseUrl) {
342
505
  mimeType: 'application/json',
343
506
  text: JSON.stringify({
344
507
  name: 'dero-daemon-mcp',
345
- version: '0.1.0',
508
+ version: '0.2.2',
346
509
  mode: 'read-only',
347
510
  endpoint: endpoint,
511
+ docs_products: DERO_DOC_PRODUCTS,
512
+ docs_delivery: 'bundled-index',
513
+ docs_dev_override_env: 'DERO_DOCS_ROOT',
348
514
  tools: DERO_TOOL_NAMES,
349
515
  resources: DERO_RESOURCE_URIS,
350
516
  prompts: DERO_PROMPT_NAMES,
@@ -378,7 +544,7 @@ export function createDeroMcpServer(daemonBaseUrl) {
378
544
  ],
379
545
  }));
380
546
  server.registerResource('dero_mcp_example_flows', 'dero://mcp/example-flows', {
381
- description: 'Compact agent flow recipes for common DERO investigations.',
547
+ description: 'Compact agent flow recipes for common DERO investigations. Composites are listed FIRST; primitives are the fallback path.',
382
548
  mimeType: 'text/markdown',
383
549
  }, async (uri) => ({
384
550
  contents: [
@@ -388,16 +554,98 @@ export function createDeroMcpServer(daemonBaseUrl) {
388
554
  text: [
389
555
  '# DERO MCP Example Flows',
390
556
  '',
391
- '- Network health: `dero_daemon_ping` -> `dero_get_info` -> `dero_get_height`',
392
- `- Inspect SC state: \`dero_get_sc\` with SCID (name registry: \`${NAME_REGISTRY_SCID}\`)`,
393
- '- Trace transaction: `dero_get_transaction` with `decode_as_json: 1`',
394
- '- Read-only boundary: no wallet writes or raw tx submission',
557
+ 'Prefer composites each is one call replacing a primitive chain, and each returns a narrative + curated docs citations.',
558
+ '',
559
+ '## Composites (preferred)',
560
+ '',
561
+ '- **Network health**: call `diagnose_chain_health` (no args). Returns narrative + signals + citations in one shot.',
562
+ `- **Inspect a contract**: call \`explain_smart_contract\` with the SCID. For example, the name registry: \`${NAME_REGISTRY_SCID}\`.`,
563
+ '- **Trace a transaction**: call `trace_transaction_with_context` with the tx_hash. Handles SC install surface extraction inline.',
564
+ '- **Find the right docs**: call `recommend_docs_path` with a natural-language intent (e.g. "deploy a TELA app"). Optional `product_hint` biases the score 1.5x toward that product.',
565
+ '- **Pre-flight a deploy**: call `estimate_deploy_cost` with the DVM-BASIC source. Returns gas estimate + plain-text breakdown + parsed surface.',
566
+ '',
567
+ '## Primitive fallback paths (only when a composite is unavailable or returns _meta.error)',
568
+ '',
569
+ '- Network: `dero_daemon_ping` → `dero_get_info` → `dero_get_height` → `dero_get_tx_pool`',
570
+ '- Contract: `dero_get_sc` (code=true, variables=true) then optionally `dero_docs_get_page`',
571
+ '- Transaction: `dero_get_transaction` (decode_as_json=1) — does NOT decode SC invocation args',
572
+ '- Docs: `dero_docs_search` (then `dero_docs_get_page` for full text)',
573
+ '- Deploy estimate: `dero_get_gas_estimate`',
574
+ '',
575
+ '## Structured error codes (`_meta.error.code`) the agent should react to',
576
+ '',
577
+ '- `NO_DOCS_MATCH` (recommend_docs_path): rephrase the intent, retry. Not a hard failure.',
578
+ '- `INVALID_INPUT` (estimate_deploy_cost): the daemon\'s raw -32098 compile message is in `_meta.error.raw`; surface it to the user.',
579
+ '- `TX_NOT_FOUND` (trace_transaction_with_context): the daemon returned an empty record. Retryable=true (mempool propagation), but only after verifying the hash and network.',
580
+ '',
581
+ '## Read-only boundary',
582
+ '',
583
+ 'No wallet writes. No raw tx submission. No contract invocation. See `dero://mcp/safety-boundary` and `dero://mcp/composites` for the full posture.',
395
584
  ].join('\n'),
396
585
  },
397
586
  ],
398
587
  }));
588
+ server.registerResource('dero_mcp_composites', 'dero://mcp/composites', {
589
+ description: 'Catalog of the 5 composite tools — what each replaces, when to call it, what it returns, and which structured _meta.error codes it can emit. Read this when picking between a composite and a primitive.',
590
+ mimeType: 'application/json',
591
+ }, async (uri) => ({
592
+ contents: [
593
+ {
594
+ uri: uri.toString(),
595
+ mimeType: 'application/json',
596
+ text: JSON.stringify({
597
+ version: 1,
598
+ note: 'Composites fuse one or more daemon-read primitives with bundled-docs lookups and emit a single narrative + curated related_docs. Always prefer the composite when its intent matches the user request.',
599
+ composites: [
600
+ {
601
+ name: 'diagnose_chain_health',
602
+ replaces: ['dero_daemon_ping', 'dero_get_info', 'dero_get_height', 'dero_get_tx_pool'],
603
+ when_to_call: 'User asks "is the chain healthy", "are we synced", "what is the network state", or any general daemon-status question.',
604
+ inputs: { include_tx_pool: 'optional boolean, default true' },
605
+ output_highlights: ['status (healthy | degraded | unreachable)', 'signals (e.g. healthy, stale-tip, lagging)', 'tip metadata', 'narrative', 'related_docs'],
606
+ error_codes: ['RPC_UNREACHABLE'],
607
+ },
608
+ {
609
+ name: 'explain_smart_contract',
610
+ replaces: ['dero_get_sc + manual parsing + dero_docs_search'],
611
+ when_to_call: 'User wants to UNDERSTAND a contract (functions, state shape, what DVM concept to read about). NOT for raw variable inspection — use dero_get_sc for that.',
612
+ inputs: { scid: '64-char hex SCID', topoheight: 'optional number' },
613
+ output_highlights: ['kind (token | registry | minimal | generic)', 'surface (functions, stringkeys, uint64keys, balances)', 'narrative', '1-4 curated DVM docs citations re-ranked for the contract pattern'],
614
+ error_codes: ['RPC_UNREACHABLE', 'RPC_INVALID_PARAMS'],
615
+ },
616
+ {
617
+ name: 'recommend_docs_path',
618
+ replaces: ['4x parallel dero_docs_search calls + manual ranking'],
619
+ when_to_call: 'User has a natural-language intent ("deploy a TELA app", "estimate gas") and needs to know which doc page to read. Bias-not-filter on product_hint.',
620
+ inputs: { intent: 'short natural-language string', product_hint: 'optional derod | tela | hologram | deropay', limit_per_product: 'optional number, default 2' },
621
+ output_highlights: ['recommended[] with score/boosted_score/rationale', 'summary_by_product', 'related_docs'],
622
+ error_codes: ['NO_DOCS_MATCH'],
623
+ },
624
+ {
625
+ name: 'estimate_deploy_cost',
626
+ replaces: ['dero_get_gas_estimate + manual surface extraction + manual interpretation of gascompute/gasstorage'],
627
+ when_to_call: 'User wants to deploy a contract and needs to know what it will cost. Read-only; nothing is submitted.',
628
+ inputs: { sc: 'DVM-BASIC source string', include_breakdown: 'optional boolean, default true' },
629
+ output_highlights: ['estimate (gascompute, gasstorage, total, status)', 'breakdown (compute_note, storage_note) | null', 'surface (functions, stringkeys, uint64keys)'],
630
+ error_codes: ['INVALID_INPUT (wraps daemon -32098 DVM compile errors; raw message in _meta.error.raw)', 'RPC_UNREACHABLE'],
631
+ },
632
+ {
633
+ name: 'trace_transaction_with_context',
634
+ replaces: ['dero_get_transaction + (for SC installs) dero_get_sc + manual classification'],
635
+ when_to_call: 'User asks "what is this tx", "is this confirmed", "what contract did this deploy", "what does this tx do".',
636
+ inputs: { tx_hash: '64-char hex', decode: 'optional boolean, default true', include_sc_context: 'optional boolean, default true' },
637
+ output_highlights: ['confirmation (status, block_height, valid_block, in_pool)', 'kind (sc_install | transfer_or_invocation | coinbase | unknown)', 'ring (groups, first_group_size)', 'sc_install (scid + parsed surface) | null', 'raw_tx_hex_length', 'narrative', 'related_docs'],
638
+ scope_note: 'SC invocation arg decoding is NOT performed (would require the binary tx codec). SC INSTALL surface extraction IS performed inline because the source is embedded in the tx record.',
639
+ error_codes: ['TX_NOT_FOUND (retryable=true; daemon returns empty record on unknown hashes)', 'RPC_UNREACHABLE'],
640
+ },
641
+ ],
642
+ design_contract_doc: 'docs/composites.md in the dero-mcp-server repo',
643
+ }, null, 2),
644
+ },
645
+ ],
646
+ }));
399
647
  server.registerPrompt('network_health_check', {
400
- description: 'Guide the model through a DERO daemon sync and health check sequence.',
648
+ description: 'Guide the model through a DERO daemon sync and health check using the diagnose_chain_health composite.',
401
649
  argsSchema: {
402
650
  reference_topoheight: z
403
651
  .number()
@@ -406,65 +654,134 @@ export function createDeroMcpServer(daemonBaseUrl) {
406
654
  .optional(),
407
655
  },
408
656
  }, async ({ reference_topoheight }) => ({
409
- description: 'Prompt for sync health investigation.',
657
+ description: 'Prompt for sync health investigation (composite-first).',
410
658
  messages: [
411
659
  {
412
660
  role: 'user',
413
661
  content: {
414
662
  type: 'text',
415
663
  text: [
416
- 'Check DERO daemon health using MCP tools.',
417
- '1) Call dero_daemon_ping.',
418
- '2) Call dero_get_info and dero_get_height.',
419
- '3) Report topoheight, stableheight, version, and network.',
664
+ 'Check DERO daemon health using the MCP composite tools (one call replaces the old four-step chain).',
665
+ '',
666
+ '1) Call diagnose_chain_health with no arguments (or include_tx_pool=true if you specifically want mempool counts).',
667
+ '2) Read the returned narrative aloud; it already summarizes ping latency, topoheight, stableheight, version, network, and mempool state.',
668
+ '3) Inspect signals (e.g. "healthy", "stale-tip", "lagging") and surface any that are not "healthy".',
669
+ '4) Quote the related_docs citations so the user knows where to read further.',
420
670
  reference_topoheight
421
- ? `4) Compare topoheight against reference_topoheight=${reference_topoheight}.`
422
- : '4) If no reference topoheight is provided, state that external comparison is still needed for final sync confidence.',
671
+ ? `5) Compare the returned topoheight against reference_topoheight=${reference_topoheight} and report the delta.`
672
+ : '5) If no reference topoheight was provided, state that external comparison is still needed for final sync confidence.',
673
+ '',
674
+ 'Fallback: only chain primitives manually (dero_daemon_ping → dero_get_info → dero_get_height → dero_get_tx_pool) if diagnose_chain_health is unavailable or returns _meta.error.',
423
675
  ].join('\n'),
424
676
  },
425
677
  },
426
678
  ],
427
679
  }));
428
680
  server.registerPrompt('inspect_smart_contract', {
429
- description: 'Inspect contract code/variables and explain likely state model.',
681
+ description: 'Inspect a DERO contract via the explain_smart_contract composite (function surface + classification + curated DVM docs).',
430
682
  argsSchema: {
431
683
  scid: hex64Schema,
432
684
  },
433
685
  }, async ({ scid }) => ({
434
- description: 'Prompt for smart contract inspection.',
686
+ description: 'Prompt for smart contract inspection (composite-first).',
435
687
  messages: [
436
688
  {
437
689
  role: 'user',
438
690
  content: {
439
691
  type: 'text',
440
692
  text: [
441
- `Inspect DERO smart contract ${scid}.`,
442
- '1) Call dero_get_sc with variables=true and code=true.',
443
- '2) Summarize key stringkeys and balances.',
444
- '3) Explain likely data model and any assumptions.',
445
- '4) Include topoheight context from response.',
693
+ `Investigate DERO smart contract ${scid} using the MCP composite tools.`,
694
+ '',
695
+ `1) Call explain_smart_contract with scid="${scid}". This single call returns the parsed function surface, a contract kind classification (token | registry | minimal | generic), a plain-language narrative, and 1-4 DVM docs citations ranked for the contract pattern.`,
696
+ '2) Quote the narrative as-is — it already explains the likely data model, state keys, and where to read next.',
697
+ '3) If the user wants raw state (variable values, balances), THEN call dero_get_sc with variables=true and code=true as a follow-up; explain why you needed the second call.',
698
+ '4) If you want documentation for a DVM concept the contract uses, call dero_docs_get_page with the slug from one of the related_docs entries.',
699
+ '',
700
+ 'Fallback: call dero_get_sc manually only if explain_smart_contract is unavailable or returns _meta.error.',
446
701
  ].join('\n'),
447
702
  },
448
703
  },
449
704
  ],
450
705
  }));
451
706
  server.registerPrompt('trace_transaction', {
452
- description: 'Trace one transaction and summarize confirmation + SC activity.',
707
+ description: 'Trace one transaction via the trace_transaction_with_context composite (confirmation + kind classification + SC install surface).',
453
708
  argsSchema: {
454
709
  tx_hash: hex64Schema,
455
710
  },
456
711
  }, async ({ tx_hash }) => ({
457
- description: 'Prompt for transaction tracing.',
712
+ description: 'Prompt for transaction tracing (composite-first).',
713
+ messages: [
714
+ {
715
+ role: 'user',
716
+ content: {
717
+ type: 'text',
718
+ text: [
719
+ `Trace DERO transaction ${tx_hash} using the MCP composite tools.`,
720
+ '',
721
+ `1) Call trace_transaction_with_context with tx_hash="${tx_hash}". The response gives you confirmation status (confirmed | mempool | unknown), block height + valid_block, kind classification (sc_install | transfer_or_invocation | coinbase | unknown), ring stats, and — if the tx is a contract install — the parsed function surface inline (no second call needed).`,
722
+ '2) Read the returned narrative aloud. Quote the related_docs citations.',
723
+ '3) If confirmation is "mempool", explicitly note that the result is provisional and tell the user when to retry.',
724
+ '4) If _meta.error.code is TX_NOT_FOUND, do NOT retry blindly — verify the hash, confirm the network (mainnet vs testnet), and only retry if the tx was just broadcast.',
725
+ '5) Note: SC invocation arg decoding is NOT performed by the composite (would require the binary tx codec). If the tx is a non-install SC call and the user needs the entrypoint + args, surface that limitation and suggest a wallet-side decoder.',
726
+ '',
727
+ 'Fallback: call dero_get_transaction manually only if trace_transaction_with_context is unavailable or returns an unhandled _meta.error.',
728
+ ].join('\n'),
729
+ },
730
+ },
731
+ ],
732
+ }));
733
+ server.registerPrompt('find_dero_docs_for_intent', {
734
+ description: 'Find the right DERO documentation page(s) for a natural-language intent via the recommend_docs_path composite.',
735
+ argsSchema: {
736
+ intent: z.string().min(3, 'Provide a short intent like "deploy a TELA app" or "estimate gas"'),
737
+ product_hint: z.enum(DERO_DOC_PRODUCTS).optional(),
738
+ },
739
+ }, async ({ intent, product_hint }) => ({
740
+ description: 'Prompt for routing an agent intent to the right DERO docs.',
741
+ messages: [
742
+ {
743
+ role: 'user',
744
+ content: {
745
+ type: 'text',
746
+ text: [
747
+ `Find the best DERO documentation page(s) for the intent: "${intent}".`,
748
+ '',
749
+ `1) Call recommend_docs_path with intent="${intent}"${product_hint ? `, product_hint="${product_hint}" (this biases the score 1.5x toward that product; it does NOT filter the other three out)` : ' (no product_hint — all four DERO products will be searched in parallel)'}.`,
750
+ '2) Read the top 2-3 recommendations to the user with their rationale strings; quote the canonical URLs.',
751
+ '3) If the user wants the full content of any page, call dero_docs_get_page with the slug + product from the recommendation.',
752
+ '4) If _meta.error.code is NO_DOCS_MATCH, rephrase the intent (drop verbs, use product nouns like "TELA app" or "DVM contract") and call again. Do NOT just give up.',
753
+ '',
754
+ 'Prefer this composite over chaining dero_docs_search yourself across four products.',
755
+ ].join('\n'),
756
+ },
757
+ },
758
+ ],
759
+ }));
760
+ server.registerPrompt('estimate_deploy_for_contract', {
761
+ description: 'Run gas pre-flight for a DVM-BASIC contract source via the estimate_deploy_cost composite (numeric estimate + plain-text breakdown + parsed surface).',
762
+ argsSchema: {
763
+ sc_source: z.string().min(20, 'Provide DVM-BASIC contract source (at minimum: a Function/End Function block)'),
764
+ include_breakdown: z.boolean().optional(),
765
+ },
766
+ }, async ({ sc_source, include_breakdown }) => ({
767
+ description: 'Prompt for DVM deploy pre-flight (composite-first).',
458
768
  messages: [
459
769
  {
460
770
  role: 'user',
461
771
  content: {
462
772
  type: 'text',
463
773
  text: [
464
- `Trace DERO transaction ${tx_hash}.`,
465
- '1) Call dero_get_transaction with txs_hashes=[tx_hash] and decode_as_json=1.',
466
- '2) Summarize confirmation status, block height, transfers, and SC invokes.',
467
- '3) If not confirmed, mention mempool status uncertainty and next check timing.',
774
+ 'Run a deploy pre-flight (gas estimate) for the DVM-BASIC source the user supplied. This is read-only; nothing is submitted to chain.',
775
+ '',
776
+ `1) Call estimate_deploy_cost with the contract source as sc${include_breakdown === false ? ' and include_breakdown=false (caller does NOT want the plain-text gas notes)' : ' (include_breakdown defaults to true)'}.`,
777
+ '2) Quote estimate.gascompute, estimate.gasstorage, estimate.total, and the daemon\'s status string.',
778
+ '3) If include_breakdown is true, read the breakdown.compute_note and breakdown.storage_note as plain-language explanations.',
779
+ '4) Quote the parsed function surface (functions[].name) so the user can sanity-check the contract.',
780
+ '5) If _meta.error.code is INVALID_INPUT, read the hint and the raw -32098 compile message from _meta.error.raw verbatim — that tells the user what to fix in the source.',
781
+ '',
782
+ `Source (${sc_source.length} chars) starts with: ${sc_source.slice(0, 120)}${sc_source.length > 120 ? '...' : ''}`,
783
+ '',
784
+ 'Fallback: call dero_get_gas_estimate manually only if estimate_deploy_cost is unavailable.',
468
785
  ].join('\n'),
469
786
  },
470
787
  },