Skip to content

Commit 7e8c1a3

Browse files
committed
fix: address PR review feedback for on-chain tools
- Add enum constraint to all network properties in tool schemas - Validate KLEVER_NETWORK env var with fallback and warning - Truncate large string args in debug logging (prevents wasmHex flooding) - Save/restore global.fetch in test files for proper isolation - Add deploy_sc and invoke_sc local-mode integration tests - Add getKDAInfo unit tests (happy path, URL encoding, error)
1 parent 45b1e6b commit 7e8c1a3

File tree

4 files changed

+148
-16
lines changed

4 files changed

+148
-16
lines changed

src/chain/client.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { jest } from '@jest/globals';
22
import { KleverChainClient, NETWORK_CONFIGS } from './client.js';
33

4-
// Mock global fetch
4+
// Mock global fetch (save original and restore in afterAll)
5+
const originalFetch = global.fetch;
56
const mockFetch = jest.fn<typeof fetch>();
67
global.fetch = mockFetch;
78

9+
afterAll(() => {
10+
global.fetch = originalFetch;
11+
});
12+
813
function jsonResponse(data: unknown, status = 200): Response {
914
return {
1015
ok: status >= 200 && status < 300,
@@ -187,6 +192,47 @@ describe('KleverChainClient', () => {
187192
});
188193
});
189194

195+
describe('getKDAInfo', () => {
196+
it('fetches KDA token info for an address', async () => {
197+
const kdaData = {
198+
balance: 500000,
199+
frozenBalance: 0,
200+
lastClaim: { timestamp: 0, epoch: 0 },
201+
};
202+
203+
mockFetch.mockResolvedValueOnce(
204+
jsonResponse({ data: kdaData, error: '', code: 'successful' })
205+
);
206+
207+
const result = await client.getKDAInfo('klv1test', 'USDT-A1B2');
208+
expect(result.balance).toBe(500000);
209+
expect(mockFetch).toHaveBeenCalledWith(
210+
'https://node.testnet.klever.org/address/klv1test/kda?asset=USDT-A1B2',
211+
expect.anything()
212+
);
213+
});
214+
215+
it('URL-encodes the asset ID', async () => {
216+
mockFetch.mockResolvedValueOnce(
217+
jsonResponse({ data: { balance: 100 }, error: '', code: 'successful' })
218+
);
219+
220+
await client.getKDAInfo('klv1test', 'LPKLVKFI-3I0N');
221+
expect(mockFetch).toHaveBeenCalledWith(
222+
'https://node.testnet.klever.org/address/klv1test/kda?asset=LPKLVKFI-3I0N',
223+
expect.anything()
224+
);
225+
});
226+
227+
it('throws on API error', async () => {
228+
mockFetch.mockResolvedValueOnce(
229+
jsonResponse({ data: null, error: 'asset not found', code: 'internal_issue' })
230+
);
231+
232+
await expect(client.getKDAInfo('klv1test', 'FAKE-TOKEN')).rejects.toThrow('asset not found');
233+
});
234+
});
235+
190236
describe('querySmartContract', () => {
191237
it('sends VM query and returns result', async () => {
192238
const vmResult = {

src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,17 @@ function createStorageAndService() {
3838
return { storageType, contextService };
3939
}
4040

41+
const VALID_NETWORKS = new Set(['mainnet', 'testnet', 'devnet', 'local']);
42+
4143
function createChainClient(): KleverChainClient {
42-
const network = (process.env.KLEVER_NETWORK as KleverNetwork) || 'mainnet';
44+
const envNetwork = process.env.KLEVER_NETWORK;
45+
if (envNetwork && !VALID_NETWORKS.has(envNetwork)) {
46+
console.error(
47+
`[WARN] Invalid KLEVER_NETWORK="${envNetwork}". Valid: mainnet, testnet, devnet, local. Defaulting to mainnet.`
48+
);
49+
}
50+
const network: KleverNetwork =
51+
envNetwork && VALID_NETWORKS.has(envNetwork) ? (envNetwork as KleverNetwork) : 'mainnet';
4352
return new KleverChainClient({
4453
network,
4554
nodeUrl: process.env.KLEVER_NODE_URL,

src/mcp/server.test.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@ import { InMemoryStorage } from '../storage/memory.js';
66
import { KleverMCPServer } from './server.js';
77
import { KleverChainClient } from '../chain/index.js';
88

9-
// Mock global fetch for chain tools
9+
// Mock global fetch for chain tools (save original and restore in afterAll)
10+
const originalFetch = global.fetch;
1011
const mockFetch = jest.fn<typeof fetch>();
1112
global.fetch = mockFetch;
1213

14+
afterAll(() => {
15+
global.fetch = originalFetch;
16+
});
17+
1318
function jsonResponse(data: unknown, status = 200): Response {
1419
return {
1520
ok: status >= 200 && status < 300,
@@ -645,6 +650,73 @@ describe('KleverMCPServer (local mode)', () => {
645650
expect(parsed.message).toContain('Unsigned');
646651
});
647652

653+
it('deploy_sc builds unsigned deploy transaction', async () => {
654+
mockFetch.mockResolvedValueOnce(
655+
jsonResponse({ data: { nonce: 3 }, error: '', code: 'successful' })
656+
);
657+
mockFetch.mockResolvedValueOnce(
658+
jsonResponse({
659+
data: { result: { txHash: 'deployhash', tx: 'deploy_proto' } },
660+
error: '',
661+
code: 'successful',
662+
})
663+
);
664+
665+
const result = await client.callTool({
666+
name: 'deploy_sc',
667+
arguments: { sender: 'klv1deployer', wasmHex: 'deadbeefcafe' },
668+
});
669+
670+
const content = result.content as Array<{ type: string; text: string }>;
671+
const parsed = JSON.parse(content[0].text);
672+
expect(parsed.success).toBe(true);
673+
expect(parsed.txHash).toBe('deployhash');
674+
expect(parsed.unsignedTx).toBe('deploy_proto');
675+
expect(parsed.details.sender).toBe('klv1deployer');
676+
expect(parsed.details.wasmSize).toBe('6 bytes');
677+
expect(parsed.details.nonce).toBe(3);
678+
expect(parsed.nextSteps).toBeDefined();
679+
expect(parsed.message).toContain('deploy');
680+
});
681+
682+
it('invoke_sc builds unsigned invoke transaction', async () => {
683+
mockFetch.mockResolvedValueOnce(
684+
jsonResponse({ data: { nonce: 7 }, error: '', code: 'successful' })
685+
);
686+
mockFetch.mockResolvedValueOnce(
687+
jsonResponse({
688+
data: { result: { txHash: 'invokehash', tx: 'invoke_proto' } },
689+
error: '',
690+
code: 'successful',
691+
})
692+
);
693+
694+
const result = await client.callTool({
695+
name: 'invoke_sc',
696+
arguments: {
697+
sender: 'klv1caller',
698+
scAddress: 'klv1contract',
699+
funcName: 'doSomething',
700+
args: ['AQID'],
701+
value: 1000000,
702+
},
703+
});
704+
705+
const content = result.content as Array<{ type: string; text: string }>;
706+
const parsed = JSON.parse(content[0].text);
707+
expect(parsed.success).toBe(true);
708+
expect(parsed.txHash).toBe('invokehash');
709+
expect(parsed.unsignedTx).toBe('invoke_proto');
710+
expect(parsed.details.sender).toBe('klv1caller');
711+
expect(parsed.details.scAddress).toBe('klv1contract');
712+
expect(parsed.details.funcName).toBe('doSomething');
713+
expect(parsed.details.argsCount).toBe(1);
714+
expect(parsed.details.value).toBe(1000000);
715+
expect(parsed.details.nonce).toBe(7);
716+
expect(parsed.nextSteps).toBeDefined();
717+
expect(parsed.message).toContain('invoke');
718+
});
719+
648720
it('freeze_klv builds unsigned transaction', async () => {
649721
mockFetch.mockResolvedValueOnce(
650722
jsonResponse({ data: { nonce: 5 }, error: '', code: 'successful' })

src/mcp/server.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ export class KleverMCPServer {
322322
description:
323323
'Optional KDA token ID (e.g. "USDT-A1B2", "LPKLVKFI-3I0N"). Omit for KLV balance.',
324324
},
325-
network: { type: 'string', description: networkDesc },
325+
network: { type: 'string', enum: ['mainnet', 'testnet', 'devnet', 'local'], description: networkDesc },
326326
},
327327
required: ['address'],
328328
},
@@ -345,7 +345,7 @@ export class KleverMCPServer {
345345
type: 'string',
346346
description: 'Klever address (klv1... bech32 format).',
347347
},
348-
network: { type: 'string', description: networkDesc },
348+
network: { type: 'string', enum: ['mainnet', 'testnet', 'devnet', 'local'], description: networkDesc },
349349
},
350350
required: ['address'],
351351
},
@@ -369,7 +369,7 @@ export class KleverMCPServer {
369369
description:
370370
'Asset identifier (e.g. "KLV", "KFI", "USDT-A1B2", "MYNFT-XY78").',
371371
},
372-
network: { type: 'string', description: networkDesc },
372+
network: { type: 'string', enum: ['mainnet', 'testnet', 'devnet', 'local'], description: networkDesc },
373373
},
374374
required: ['assetId'],
375375
},
@@ -403,7 +403,7 @@ export class KleverMCPServer {
403403
description:
404404
'Optional base64-encoded arguments. For addresses, encode the hex-decoded bech32 bytes. For numbers, use big-endian byte encoding.',
405405
},
406-
network: { type: 'string', description: networkDesc },
406+
network: { type: 'string', enum: ['mainnet', 'testnet', 'devnet', 'local'], description: networkDesc },
407407
},
408408
required: ['scAddress', 'funcName'],
409409
},
@@ -426,7 +426,7 @@ export class KleverMCPServer {
426426
type: 'string',
427427
description: 'Transaction hash (hex string).',
428428
},
429-
network: { type: 'string', description: networkDesc },
429+
network: { type: 'string', enum: ['mainnet', 'testnet', 'devnet', 'local'], description: networkDesc },
430430
},
431431
required: ['hash'],
432432
},
@@ -451,7 +451,7 @@ export class KleverMCPServer {
451451
description:
452452
'Block number (nonce). Omit to get the latest block.',
453453
},
454-
network: { type: 'string', description: networkDesc },
454+
network: { type: 'string', enum: ['mainnet', 'testnet', 'devnet', 'local'], description: networkDesc },
455455
},
456456
},
457457
annotations: {
@@ -469,7 +469,7 @@ export class KleverMCPServer {
469469
inputSchema: {
470470
type: 'object' as const,
471471
properties: {
472-
network: { type: 'string', description: networkDesc },
472+
network: { type: 'string', enum: ['mainnet', 'testnet', 'devnet', 'local'], description: networkDesc },
473473
},
474474
},
475475
annotations: {
@@ -512,7 +512,7 @@ export class KleverMCPServer {
512512
description:
513513
'Optional KDA token ID for non-KLV transfers (e.g. "USDT-A1B2"). Omit for KLV.',
514514
},
515-
network: { type: 'string', description: networkDesc },
515+
network: { type: 'string', enum: ['mainnet', 'testnet', 'devnet', 'local'], description: networkDesc },
516516
},
517517
required: ['sender', 'receiver', 'amount'],
518518
},
@@ -546,7 +546,7 @@ export class KleverMCPServer {
546546
description:
547547
'Optional base64-encoded init arguments for the contract constructor.',
548548
},
549-
network: { type: 'string', description: networkDesc },
549+
network: { type: 'string', enum: ['mainnet', 'testnet', 'devnet', 'local'], description: networkDesc },
550550
},
551551
required: ['sender', 'wasmHex'],
552552
},
@@ -588,7 +588,7 @@ export class KleverMCPServer {
588588
description:
589589
'Optional KLV amount to send with the call (smallest unit). Required for payable endpoints.',
590590
},
591-
network: { type: 'string', description: networkDesc },
591+
network: { type: 'string', enum: ['mainnet', 'testnet', 'devnet', 'local'], description: networkDesc },
592592
},
593593
required: ['sender', 'scAddress', 'funcName'],
594594
},
@@ -617,7 +617,7 @@ export class KleverMCPServer {
617617
description:
618618
'Amount of KLV to freeze in the smallest unit (1 KLV = 1,000,000 units).',
619619
},
620-
network: { type: 'string', description: networkDesc },
620+
network: { type: 'string', enum: ['mainnet', 'testnet', 'devnet', 'local'], description: networkDesc },
621621
},
622622
required: ['sender', 'amount'],
623623
},
@@ -768,8 +768,13 @@ export class KleverMCPServer {
768768
this.server.setRequestHandler(CallToolRequestSchema, async request => {
769769
const { name, arguments: args } = request.params;
770770

771-
// Debug logging to stderr
772-
console.error(`[MCP] Tool called: ${name}`, JSON.stringify(args));
771+
// Debug logging to stderr (truncate large fields like wasmHex)
772+
const safeArgs = args ? Object.fromEntries(
773+
Object.entries(args).map(([k, v]) =>
774+
typeof v === 'string' && v.length > 200 ? [k, `${v.slice(0, 100)}...(${v.length} chars)`] : [k, v]
775+
)
776+
) : args;
777+
console.error(`[MCP] Tool called: ${name}`, JSON.stringify(safeArgs));
773778

774779
// Block local-only tools in public profile
775780
const localOnlyTools = [

0 commit comments

Comments
 (0)