Skip to content

Commit 0f659fd

Browse files
authored
fix(hdx-eval): reject invalid MCP evidence content (#3532)
Fixes #3526. `queryTileWithEvidence` now reports a failed query when an MCP transport succeeds but the returned content is not valid JSON. Malformed text no longer counts as evidence with `success: true`. Empty or whitespace-only content keeps the existing successful no-data behavior. Added regression coverage for malformed content, empty content, valid rows, and valid content without a result. Validation: - `yarn workspace @hyperdx/hdx-eval lint` - `yarn workspace @hyperdx/hdx-eval tsc --noEmit` - `yarn workspace @hyperdx/hdx-eval jest --runInBand src/__tests__/api.test.ts --coverage=false` (4 passed) - Full hdx-eval tests: 413 passed; 2 unrelated Windows path assertions failed in `src/__tests__/runPath.test.ts` because the fixture expects `\tmp\...` while Windows returns `C:\tmp\...`.
1 parent dd8c2e4 commit 0f659fd

2 files changed

Lines changed: 117 additions & 2 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { HyperdxApiClient } from '@/hyperdx/api';
2+
3+
const originalFetch = global.fetch;
4+
5+
function mockMcpContent(text: string): void {
6+
global.fetch = jest.fn().mockResolvedValue({
7+
ok: true,
8+
status: 200,
9+
text: async () =>
10+
JSON.stringify({
11+
jsonrpc: '2.0',
12+
result: {
13+
content: [{ type: 'text', text }],
14+
},
15+
}),
16+
}) as unknown as typeof fetch;
17+
}
18+
19+
afterEach(() => {
20+
global.fetch = originalFetch;
21+
});
22+
23+
describe('HyperdxApiClient', () => {
24+
it('reports invalid MCP content as a failed tile query', async () => {
25+
mockMcpContent('not valid JSON');
26+
27+
const result = await new HyperdxApiClient(
28+
'http://localhost',
29+
).queryTileWithEvidence({
30+
accessKey: 'test-key',
31+
dashboardId: 'dashboard-id',
32+
tileId: 'tile-id',
33+
startTime: '2026-01-01T00:00:00.000Z',
34+
endTime: '2026-01-01T01:00:00.000Z',
35+
});
36+
37+
expect(result.success).toBe(false);
38+
expect(result.hasData).toBe(false);
39+
expect(result.error).toMatch(/^Invalid MCP content:/);
40+
});
41+
42+
it('treats empty MCP content as a successful query with no data', async () => {
43+
mockMcpContent(' \n');
44+
45+
const result = await new HyperdxApiClient(
46+
'http://localhost',
47+
).queryTileWithEvidence({
48+
accessKey: 'test-key',
49+
dashboardId: 'dashboard-id',
50+
tileId: 'tile-id',
51+
startTime: '2026-01-01T00:00:00.000Z',
52+
endTime: '2026-01-01T01:00:00.000Z',
53+
});
54+
55+
expect(result).toEqual({ success: true, hasData: false });
56+
});
57+
58+
it('extracts rows and groups from valid MCP content', async () => {
59+
mockMcpContent(
60+
JSON.stringify({
61+
result: [
62+
{ ServiceName: 'api', value: 1 },
63+
{ ServiceName: 'worker', value: 2 },
64+
],
65+
}),
66+
);
67+
68+
const result = await new HyperdxApiClient(
69+
'http://localhost',
70+
).queryTileWithEvidence({
71+
accessKey: 'test-key',
72+
dashboardId: 'dashboard-id',
73+
tileId: 'tile-id',
74+
startTime: '2026-01-01T00:00:00.000Z',
75+
endTime: '2026-01-01T01:00:00.000Z',
76+
});
77+
78+
expect(result).toEqual({
79+
success: true,
80+
hasData: true,
81+
rowCount: 2,
82+
groupCount: 2,
83+
sampleRows: [
84+
{ ServiceName: 'api', value: 1 },
85+
{ ServiceName: 'worker', value: 2 },
86+
],
87+
});
88+
});
89+
90+
it('treats valid MCP content without a result as no data', async () => {
91+
mockMcpContent('{}');
92+
93+
const result = await new HyperdxApiClient(
94+
'http://localhost',
95+
).queryTileWithEvidence({
96+
accessKey: 'test-key',
97+
dashboardId: 'dashboard-id',
98+
tileId: 'tile-id',
99+
startTime: '2026-01-01T00:00:00.000Z',
100+
endTime: '2026-01-01T01:00:00.000Z',
101+
});
102+
103+
expect(result).toEqual({ success: true, hasData: false });
104+
});
105+
});

‎packages/hdx-eval/src/hyperdx/api.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,10 @@ export class HyperdxApiClient {
325325
};
326326
}
327327

328+
if (!content.text.trim()) {
329+
return { success: true, hasData: false };
330+
}
331+
328332
try {
329333
const inner = JSON.parse(content.text);
330334
const result = inner.result;
@@ -368,8 +372,14 @@ export class HyperdxApiClient {
368372
groupCount,
369373
sampleRows: rows.slice(0, 5),
370374
};
371-
} catch {
372-
return { success: true, hasData: content.text.length > 0 };
375+
} catch (error) {
376+
return {
377+
success: false,
378+
hasData: false,
379+
error: `Invalid MCP content: ${
380+
error instanceof Error ? error.message : String(error)
381+
}`.slice(0, 300),
382+
};
373383
}
374384
}
375385
}

0 commit comments

Comments
 (0)