@moda-ai/cli 1.0.4 → 1.1.0

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 (45) hide show
  1. package/.env.example +2 -2
  2. package/README.md +52 -50
  3. package/dist/auth.js +39 -67
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +140 -14
  6. package/dist/config.d.ts +1 -2
  7. package/dist/config.js +3 -4
  8. package/dist/init/detect.d.ts +1 -1
  9. package/dist/init/detect.js +28 -5
  10. package/dist/init/index.js +26 -68
  11. package/dist/init/mcp.d.ts +1 -7
  12. package/dist/init/mcp.js +5 -88
  13. package/dist/init/rules.d.ts +1 -1
  14. package/dist/init/rules.js +151 -58
  15. package/dist/init/tenant.js +4 -42
  16. package/dist/schemas.d.ts +71 -0
  17. package/dist/schemas.js +44 -0
  18. package/dist/transforms.d.ts +25 -0
  19. package/dist/transforms.js +102 -0
  20. package/package.json +8 -19
  21. package/skills/integrate-moda/SKILL.md +63 -0
  22. package/skills/moda-cli/SKILL.md +323 -0
  23. package/dist/__tests__/api-client.test.d.ts +0 -2
  24. package/dist/__tests__/api-client.test.js +0 -73
  25. package/dist/__tests__/cli.test.d.ts +0 -2
  26. package/dist/__tests__/cli.test.js +0 -109
  27. package/dist/__tests__/e2e.test.d.ts +0 -2
  28. package/dist/__tests__/e2e.test.js +0 -239
  29. package/dist/__tests__/handlers.test.d.ts +0 -2
  30. package/dist/__tests__/handlers.test.js +0 -71
  31. package/dist/__tests__/integration.test.d.ts +0 -2
  32. package/dist/__tests__/integration.test.js +0 -114
  33. package/dist/__tests__/schemas.test.d.ts +0 -2
  34. package/dist/__tests__/schemas.test.js +0 -81
  35. package/dist/handlers.d.ts +0 -7
  36. package/dist/handlers.js +0 -117
  37. package/dist/http-server.d.ts +0 -3
  38. package/dist/http-server.js +0 -74
  39. package/dist/index.d.ts +0 -3
  40. package/dist/index.js +0 -26
  41. package/dist/tools.d.ts +0 -260
  42. package/dist/tools.js +0 -133
  43. package/dist/worker.d.ts +0 -9
  44. package/dist/worker.js +0 -24
  45. package/mcp-config.example.json +0 -12
@@ -1,73 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- // Mock config before importing api-client
3
- vi.mock('../config.js', () => ({
4
- MODA_API_KEY: 'moda_sk_test_key',
5
- MODA_BASE_URL: 'https://test.modaflows.com',
6
- }));
7
- import { callDataAPI, ApiError } from '../api-client.js';
8
- describe('callDataAPI', () => {
9
- beforeEach(() => {
10
- vi.restoreAllMocks();
11
- });
12
- it('constructs correct URL', async () => {
13
- const mockFetch = vi.fn().mockResolvedValue({
14
- ok: true,
15
- json: () => Promise.resolve({ data: 'test' }),
16
- });
17
- vi.stubGlobal('fetch', mockFetch);
18
- await callDataAPI('/overview?days_back=7');
19
- expect(mockFetch).toHaveBeenCalledWith('https://test.modaflows.com/api/v1/data/overview?days_back=7', expect.objectContaining({
20
- headers: expect.objectContaining({
21
- 'x-api-key': 'moda_sk_test_key',
22
- }),
23
- }));
24
- });
25
- it('throws ApiError on non-ok response', async () => {
26
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
27
- ok: false,
28
- status: 401,
29
- text: () => Promise.resolve('Unauthorized'),
30
- }));
31
- await expect(callDataAPI('/overview')).rejects.toThrow('API error (401): Unauthorized');
32
- await vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
33
- ok: false,
34
- status: 401,
35
- text: () => Promise.resolve('Unauthorized'),
36
- }));
37
- try {
38
- await callDataAPI('/overview');
39
- }
40
- catch (e) {
41
- expect(e).toBeInstanceOf(ApiError);
42
- expect(e.statusCode).toBe(401);
43
- }
44
- });
45
- it('retries on 500 errors', async () => {
46
- const mockFetch = vi.fn()
47
- .mockResolvedValueOnce({ ok: false, status: 500, text: () => Promise.resolve('Internal Server Error') })
48
- .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ data: 'ok' }) });
49
- vi.stubGlobal('fetch', mockFetch);
50
- const result = await callDataAPI('/overview');
51
- expect(result).toEqual({ data: 'ok' });
52
- expect(mockFetch).toHaveBeenCalledTimes(2);
53
- });
54
- it('does not retry on 4xx errors (except 429)', async () => {
55
- const mockFetch = vi.fn().mockResolvedValue({
56
- ok: false,
57
- status: 403,
58
- text: () => Promise.resolve('Forbidden'),
59
- });
60
- vi.stubGlobal('fetch', mockFetch);
61
- await expect(callDataAPI('/overview')).rejects.toThrow('API error (403)');
62
- expect(mockFetch).toHaveBeenCalledTimes(1);
63
- });
64
- it('returns parsed JSON on success', async () => {
65
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
66
- ok: true,
67
- json: () => Promise.resolve({ conversations: { total: 100 } }),
68
- }));
69
- const result = await callDataAPI('/overview');
70
- expect(result).toEqual({ conversations: { total: 100 } });
71
- });
72
- });
73
- //# sourceMappingURL=api-client.test.js.map
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=cli.test.d.ts.map
@@ -1,109 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- // Mock api-client (same pattern as handlers.test.ts)
3
- vi.mock('../api-client.js', () => ({
4
- callDataAPI: vi.fn().mockResolvedValue({ mock: true }),
5
- }));
6
- // Mock config to prevent process.exit
7
- vi.mock('../config.js', () => ({
8
- validateConfig: vi.fn(),
9
- MODA_API_KEY: 'test-key',
10
- MODA_BASE_URL: 'https://test.example.com',
11
- PORT: 3003,
12
- }));
13
- // Import after mocks
14
- import { parseArgs, flagsToArgs } from '../cli.js';
15
- import { callDataAPI } from '../api-client.js';
16
- const mockCallDataAPI = vi.mocked(callDataAPI);
17
- describe('parseArgs', () => {
18
- it('extracts command from argv', () => {
19
- const result = parseArgs(['node', 'cli.js', 'overview']);
20
- expect(result.command).toBe('overview');
21
- expect(result.positional).toBeUndefined();
22
- expect(result.flags).toEqual({});
23
- });
24
- it('extracts positional argument', () => {
25
- const result = parseArgs(['node', 'cli.js', 'cluster-conversations', 'node-123']);
26
- expect(result.command).toBe('cluster-conversations');
27
- expect(result.positional).toBe('node-123');
28
- });
29
- it('parses --key=value flags', () => {
30
- const result = parseArgs(['node', 'cli.js', 'overview', '--days-back=30']);
31
- expect(result.flags).toEqual({ 'days-back': '30' });
32
- });
33
- it('parses --key value flags', () => {
34
- const result = parseArgs(['node', 'cli.js', 'overview', '--days-back', '30']);
35
- expect(result.flags).toEqual({ 'days-back': '30' });
36
- });
37
- it('parses mixed flag formats', () => {
38
- const result = parseArgs(['node', 'cli.js', 'conversations', '--search=error', '--limit', '5']);
39
- expect(result.flags).toEqual({ search: 'error', limit: '5' });
40
- });
41
- it('handles boolean flags (no value)', () => {
42
- const result = parseArgs(['node', 'cli.js', '--help']);
43
- expect(result.command).toBe('--help');
44
- });
45
- it('returns empty command for no args', () => {
46
- const result = parseArgs(['node', 'cli.js']);
47
- expect(result.command).toBe('');
48
- });
49
- it('handles positional before flags', () => {
50
- const result = parseArgs(['node', 'cli.js', 'context', 'conv-abc', '--window=3']);
51
- expect(result.command).toBe('context');
52
- expect(result.positional).toBe('conv-abc');
53
- expect(result.flags).toEqual({ window: '3' });
54
- });
55
- });
56
- describe('flagsToArgs', () => {
57
- it('converts kebab-case to snake_case', () => {
58
- const result = flagsToArgs({ 'days-back': '30', 'time-range': '7d' });
59
- expect(result).toEqual({ days_back: 30, time_range: '7d' });
60
- });
61
- it('parses numeric values', () => {
62
- const result = flagsToArgs({ limit: '10', offset: '0' });
63
- expect(result).toEqual({ limit: 10, offset: 0 });
64
- });
65
- it('keeps string values as strings', () => {
66
- const result = flagsToArgs({ search: 'error message' });
67
- expect(result).toEqual({ search: 'error message' });
68
- });
69
- it('includes positional key/value', () => {
70
- const result = flagsToArgs({ limit: '5' }, 'node_id', 'node-123');
71
- expect(result).toEqual({ node_id: 'node-123', limit: 5 });
72
- });
73
- });
74
- describe('CLI command dispatch', () => {
75
- beforeEach(() => {
76
- vi.clearAllMocks();
77
- });
78
- // We test the commands by dynamically importing and calling runCommand.
79
- // Since runCommand is not exported, we test indirectly via the module's behavior.
80
- // The key thing to test is that each command constructs the correct API URL.
81
- // This is tested at the integration level via handlers.test.ts already.
82
- // Here we focus on the arg parsing + flag conversion that is unique to the CLI.
83
- it('overview command builds correct args from flags', () => {
84
- const args = flagsToArgs({ 'days-back': '14' });
85
- expect(args).toEqual({ days_back: 14 });
86
- });
87
- it('cluster-conversations builds args with positional node_id', () => {
88
- const args = flagsToArgs({ limit: '5', offset: '0' }, 'node_id', 'cluster-abc');
89
- expect(args).toEqual({ node_id: 'cluster-abc', limit: 5, offset: 0 });
90
- });
91
- it('conversations builds args with multiple flags', () => {
92
- const args = flagsToArgs({ search: 'error', 'user-id': 'user-1', 'time-range': '7d', limit: '10', offset: '0' });
93
- expect(args).toEqual({ search: 'error', user_id: 'user-1', time_range: '7d', limit: 10, offset: 0 });
94
- });
95
- it('context builds args with positional conversation_id', () => {
96
- const args = flagsToArgs({ 'msg-index': '5', window: '3' }, 'conversation_id', 'conv-xyz');
97
- expect(args).toEqual({ conversation_id: 'conv-xyz', msg_index: 5, window: 3 });
98
- });
99
- it('tool-failure-detail builds args with positional tool_name', () => {
100
- const args = flagsToArgs({ subtype: 'timeout', 'days-back': '14' }, 'tool_name', 'search');
101
- expect(args).toEqual({ tool_name: 'search', subtype: 'timeout', days_back: 14 });
102
- });
103
- it('offset=0 is preserved (not dropped as falsy)', () => {
104
- const args = flagsToArgs({ offset: '0' });
105
- expect(args.offset).toBe(0);
106
- expect(args.offset).not.toBeUndefined();
107
- });
108
- });
109
- //# sourceMappingURL=cli.test.js.map
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=e2e.test.d.ts.map
@@ -1,239 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { resolve, dirname } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
5
- const __dirname = dirname(fileURLToPath(import.meta.url));
6
- const SERVER_PATH = resolve(__dirname, '../../dist/index.js');
7
- const MODA_API_KEY = process.env.MODA_API_KEY;
8
- // --- JSON-RPC Helper ---
9
- let nextId = 1;
10
- function sendJsonRpc(server, method, params) {
11
- const id = nextId++;
12
- return new Promise((resolve, reject) => {
13
- let buffer = '';
14
- const onData = (chunk) => {
15
- buffer += chunk.toString();
16
- const lines = buffer.split('\n');
17
- // Keep the last (potentially incomplete) line in the buffer
18
- buffer = lines.pop() || '';
19
- for (const line of lines) {
20
- if (!line.trim())
21
- continue;
22
- try {
23
- const parsed = JSON.parse(line);
24
- if (parsed.id === id) {
25
- server.stdout.removeListener('data', onData);
26
- resolve(parsed);
27
- }
28
- }
29
- catch {
30
- // Not valid JSON, skip
31
- }
32
- }
33
- };
34
- server.stdout.on('data', onData);
35
- const message = JSON.stringify({
36
- jsonrpc: '2.0',
37
- method,
38
- ...(params && { params }),
39
- id,
40
- });
41
- server.stdin.write(message + '\n');
42
- // Timeout after 30s
43
- setTimeout(() => {
44
- server.stdout.removeListener('data', onData);
45
- reject(new Error(`sendJsonRpc timed out waiting for response id=${id}`));
46
- }, 30_000);
47
- });
48
- }
49
- // --- Shared State for Chained Tests ---
50
- let capturedNodeId;
51
- let capturedConversationId;
52
- let capturedToolName;
53
- // --- Test Suite ---
54
- describe.skipIf(!MODA_API_KEY)('E2E: MCP Server against live backend', { timeout: 30_000 }, () => {
55
- let mcpServer;
56
- beforeAll(async () => {
57
- // Spawn the MCP server process
58
- mcpServer = spawn('node', [SERVER_PATH], {
59
- env: { ...process.env },
60
- stdio: ['pipe', 'pipe', 'pipe'],
61
- });
62
- // Wait for the server startup message on stderr
63
- await new Promise((resolve, reject) => {
64
- const timeout = setTimeout(() => reject(new Error('Server startup timeout')), 10_000);
65
- mcpServer.stderr.on('data', (data) => {
66
- if (data.toString().includes('running on stdio')) {
67
- clearTimeout(timeout);
68
- resolve();
69
- }
70
- });
71
- mcpServer.on('error', (err) => {
72
- clearTimeout(timeout);
73
- reject(err);
74
- });
75
- });
76
- // MCP protocol: send initialize request
77
- const initResponse = await sendJsonRpc(mcpServer, 'initialize', {
78
- protocolVersion: '2024-11-05',
79
- capabilities: {},
80
- clientInfo: { name: 'e2e-test', version: '1.0.0' },
81
- });
82
- expect(initResponse.result).toBeDefined();
83
- // MCP protocol: send initialized notification (no id, no response)
84
- mcpServer.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
85
- });
86
- afterAll(() => {
87
- mcpServer?.kill();
88
- });
89
- // --- Phase 1: Overview ---
90
- it('moda_get_overview returns valid overview data', async () => {
91
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
92
- name: 'moda_get_overview',
93
- arguments: { days_back: 7 },
94
- });
95
- expect(response.result).toBeDefined();
96
- expect(response.result.content).toHaveLength(1);
97
- expect(response.result.content[0].type).toBe('text');
98
- const data = JSON.parse(response.result.content[0].text);
99
- expect(data).toHaveProperty('conversations');
100
- expect(data).toHaveProperty('frustrations');
101
- expect(data).toHaveProperty('tool_failures');
102
- expect(data).toHaveProperty('top_clusters');
103
- expect(data).toHaveProperty('recent_activity');
104
- });
105
- // --- Phase 2: Independent Tools ---
106
- it('moda_get_clusters returns cluster hierarchy', async () => {
107
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
108
- name: 'moda_get_clusters',
109
- arguments: {},
110
- });
111
- expect(response.result).toBeDefined();
112
- const data = JSON.parse(response.result.content[0].text);
113
- expect(data).toHaveProperty('clusters');
114
- expect(Array.isArray(data.clusters)).toBe(true);
115
- if (data.clusters.length > 0) {
116
- const cluster = data.clusters[0];
117
- expect(cluster).toHaveProperty('node_id');
118
- expect(cluster).toHaveProperty('label');
119
- // Capture for Phase 3
120
- capturedNodeId = cluster.node_id;
121
- }
122
- });
123
- it('moda_search_conversations returns conversations list', async () => {
124
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
125
- name: 'moda_search_conversations',
126
- arguments: {},
127
- });
128
- expect(response.result).toBeDefined();
129
- const data = JSON.parse(response.result.content[0].text);
130
- expect(data).toHaveProperty('conversations');
131
- expect(Array.isArray(data.conversations)).toBe(true);
132
- expect(data).toHaveProperty('pagination');
133
- if (data.conversations.length > 0) {
134
- const convo = data.conversations[0];
135
- expect(convo).toHaveProperty('conversation_id');
136
- expect(convo).toHaveProperty('summary');
137
- // Capture for Phase 3
138
- capturedConversationId = convo.conversation_id;
139
- }
140
- });
141
- it('moda_get_frustrations returns frustration data', async () => {
142
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
143
- name: 'moda_get_frustrations',
144
- arguments: { days_back: 7 },
145
- });
146
- expect(response.result).toBeDefined();
147
- const data = JSON.parse(response.result.content[0].text);
148
- expect(data).toHaveProperty('summary');
149
- expect(data).toHaveProperty('frustrations');
150
- expect(Array.isArray(data.frustrations)).toBe(true);
151
- if (data.frustrations.length > 0) {
152
- expect(data.frustrations[0]).toHaveProperty('conversation_id');
153
- }
154
- });
155
- it('moda_get_tool_failures returns failure overview', async () => {
156
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
157
- name: 'moda_get_tool_failures',
158
- arguments: { days_back: 7 },
159
- });
160
- expect(response.result).toBeDefined();
161
- const data = JSON.parse(response.result.content[0].text);
162
- expect(data).toHaveProperty('summary');
163
- expect(data).toHaveProperty('tools');
164
- expect(Array.isArray(data.tools)).toBe(true);
165
- if (data.tools.length > 0) {
166
- const tool = data.tools[0];
167
- expect(tool).toHaveProperty('tool_name');
168
- expect(tool).toHaveProperty('failure_count');
169
- // Capture for Phase 3
170
- capturedToolName = tool.tool_name;
171
- }
172
- });
173
- // --- Phase 3: Chained Tools ---
174
- it('moda_get_cluster_conversations returns conversations for a cluster', async () => {
175
- if (!capturedNodeId) {
176
- console.warn('Skipping: no node_id captured from moda_get_clusters (empty data)');
177
- return;
178
- }
179
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
180
- name: 'moda_get_cluster_conversations',
181
- arguments: { node_id: capturedNodeId },
182
- });
183
- expect(response.result).toBeDefined();
184
- const data = JSON.parse(response.result.content[0].text);
185
- expect(data).toHaveProperty('cluster');
186
- expect(data).toHaveProperty('conversations');
187
- expect(Array.isArray(data.conversations)).toBe(true);
188
- expect(data).toHaveProperty('pagination');
189
- if (data.conversations.length > 0) {
190
- expect(data.conversations[0]).toHaveProperty('conversation_id');
191
- }
192
- });
193
- it('moda_get_conversation_context returns message context', async () => {
194
- if (!capturedConversationId) {
195
- console.warn('Skipping: no conversation_id captured from moda_search_conversations (empty data)');
196
- return;
197
- }
198
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
199
- name: 'moda_get_conversation_context',
200
- arguments: { conversation_id: capturedConversationId },
201
- });
202
- expect(response.result).toBeDefined();
203
- const data = JSON.parse(response.result.content[0].text);
204
- expect(data).toHaveProperty('conversation_id');
205
- expect(data).toHaveProperty('summary');
206
- expect(data).toHaveProperty('context');
207
- expect(data.context).toHaveProperty('messages');
208
- expect(Array.isArray(data.context.messages)).toBe(true);
209
- });
210
- it('moda_get_tool_failure_detail returns failure details', async () => {
211
- if (!capturedToolName) {
212
- console.warn('Skipping: no tool_name captured from moda_get_tool_failures (empty data)');
213
- return;
214
- }
215
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
216
- name: 'moda_get_tool_failure_detail',
217
- arguments: { tool_name: capturedToolName },
218
- });
219
- expect(response.result).toBeDefined();
220
- const data = JSON.parse(response.result.content[0].text);
221
- expect(data).toHaveProperty('tool_name');
222
- expect(typeof data.tool_name).toBe('string');
223
- expect(data).toHaveProperty('subtypes');
224
- expect(Array.isArray(data.subtypes)).toBe(true);
225
- expect(data).toHaveProperty('examples');
226
- expect(Array.isArray(data.examples)).toBe(true);
227
- });
228
- // --- Error Handling ---
229
- it('returns error for invalid arguments (Zod validation)', async () => {
230
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
231
- name: 'moda_get_overview',
232
- arguments: { days_back: 999 },
233
- });
234
- // The MCP SDK wraps handler errors into an error response
235
- expect(response.error).toBeDefined();
236
- expect(response.error.message).toContain('Invalid arguments');
237
- });
238
- });
239
- //# sourceMappingURL=e2e.test.js.map
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=handlers.test.d.ts.map
@@ -1,71 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- // Mock api-client
3
- vi.mock('../api-client.js', () => ({
4
- callDataAPI: vi.fn().mockResolvedValue({ mock: true }),
5
- }));
6
- import { handleToolCall } from '../handlers.js';
7
- import { callDataAPI } from '../api-client.js';
8
- const mockCallDataAPI = vi.mocked(callDataAPI);
9
- describe('handleToolCall', () => {
10
- beforeEach(() => {
11
- vi.clearAllMocks();
12
- });
13
- it('moda_get_overview calls correct endpoint', async () => {
14
- await handleToolCall('moda_get_overview', { days_back: 14 });
15
- expect(mockCallDataAPI).toHaveBeenCalledWith('/overview?days_back=14');
16
- });
17
- it('moda_get_overview with no args omits query string', async () => {
18
- await handleToolCall('moda_get_overview', {});
19
- expect(mockCallDataAPI).toHaveBeenCalledWith('/overview');
20
- });
21
- it('moda_get_clusters passes parent_id and time_range', async () => {
22
- await handleToolCall('moda_get_clusters', { parent_id: 'node-1', time_range: '7d' });
23
- expect(mockCallDataAPI).toHaveBeenCalledWith(expect.stringContaining('parent_id=node-1'));
24
- expect(mockCallDataAPI).toHaveBeenCalledWith(expect.stringContaining('time_range=7d'));
25
- });
26
- it('moda_get_cluster_conversations includes offset=0', async () => {
27
- await handleToolCall('moda_get_cluster_conversations', { node_id: 'c-1', offset: 0 });
28
- const calledUrl = mockCallDataAPI.mock.calls[0][0];
29
- expect(calledUrl).toContain('offset=0');
30
- });
31
- it('moda_search_conversations includes offset=0', async () => {
32
- await handleToolCall('moda_search_conversations', { offset: 0 });
33
- const calledUrl = mockCallDataAPI.mock.calls[0][0];
34
- expect(calledUrl).toContain('offset=0');
35
- });
36
- it('moda_get_frustrations includes offset=0', async () => {
37
- await handleToolCall('moda_get_frustrations', { offset: 0 });
38
- const calledUrl = mockCallDataAPI.mock.calls[0][0];
39
- expect(calledUrl).toContain('offset=0');
40
- });
41
- it('moda_get_tool_failure_detail includes offset=0', async () => {
42
- await handleToolCall('moda_get_tool_failure_detail', { tool_name: 'search', offset: 0 });
43
- const calledUrl = mockCallDataAPI.mock.calls[0][0];
44
- expect(calledUrl).toContain('offset=0');
45
- });
46
- it('moda_get_conversation_context uses conversation_id in path', async () => {
47
- await handleToolCall('moda_get_conversation_context', { conversation_id: 'conv-abc' });
48
- expect(mockCallDataAPI).toHaveBeenCalledWith(expect.stringContaining('/conversations/conv-abc/context'));
49
- });
50
- it('moda_get_tool_failures calls correct endpoint', async () => {
51
- await handleToolCall('moda_get_tool_failures', { days_back: 30 });
52
- expect(mockCallDataAPI).toHaveBeenCalledWith('/tool-failures?days_back=30');
53
- });
54
- it('moda_get_tool_failure_detail uses tool_name in path', async () => {
55
- await handleToolCall('moda_get_tool_failure_detail', { tool_name: 'compose' });
56
- expect(mockCallDataAPI).toHaveBeenCalledWith(expect.stringContaining('/tool-failures/compose'));
57
- });
58
- it('throws on unknown tool', async () => {
59
- await expect(handleToolCall('unknown_tool', {})).rejects.toThrow('Unknown tool: unknown_tool');
60
- });
61
- it('throws on invalid arguments', async () => {
62
- await expect(handleToolCall('moda_get_overview', { days_back: 'invalid' })).rejects.toThrow('Invalid arguments');
63
- });
64
- it('returns content array with JSON text', async () => {
65
- const result = await handleToolCall('moda_get_overview', {});
66
- expect(result.content).toHaveLength(1);
67
- expect(result.content[0].type).toBe('text');
68
- expect(JSON.parse(result.content[0].text)).toEqual({ mock: true });
69
- });
70
- });
71
- //# sourceMappingURL=handlers.test.js.map
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=integration.test.d.ts.map
@@ -1,114 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
- import { spawn } from 'child_process';
3
- import { resolve } from 'path';
4
- import { fileURLToPath } from 'url';
5
- const __dirname = fileURLToPath(new URL('.', import.meta.url));
6
- const ROOT = resolve(__dirname, '../..');
7
- let mockServer;
8
- function sendJsonRpc(proc, method, params, id = 1) {
9
- return new Promise((resolve, reject) => {
10
- const timeout = setTimeout(() => reject(new Error('Timeout waiting for response')), 10000);
11
- let buffer = '';
12
- const onData = (data) => {
13
- buffer += data.toString();
14
- const lines = buffer.split('\n');
15
- for (const line of lines) {
16
- if (!line.trim())
17
- continue;
18
- try {
19
- const parsed = JSON.parse(line);
20
- if (parsed.id === id) {
21
- clearTimeout(timeout);
22
- proc.stdout.off('data', onData);
23
- resolve(parsed);
24
- }
25
- }
26
- catch { /* incomplete JSON */ }
27
- }
28
- };
29
- proc.stdout.on('data', onData);
30
- const request = JSON.stringify({ jsonrpc: '2.0', method, params, id });
31
- proc.stdin.write(request + '\n');
32
- });
33
- }
34
- beforeAll(async () => {
35
- // Start mock server
36
- mockServer = spawn('node', [resolve(ROOT, 'mock-server.js')], { stdio: ['pipe', 'pipe', 'pipe'] });
37
- await new Promise((resolve, reject) => {
38
- const timeout = setTimeout(() => reject(new Error('Mock server did not start in time')), 10000);
39
- // mock-server.js uses console.log which goes to stdout
40
- mockServer.stdout.on('data', (data) => {
41
- if (data.toString().includes('Mock Data API server running')) {
42
- clearTimeout(timeout);
43
- resolve();
44
- }
45
- });
46
- mockServer.stderr.on('data', (data) => {
47
- if (data.toString().includes('Mock Data API server running')) {
48
- clearTimeout(timeout);
49
- resolve();
50
- }
51
- });
52
- });
53
- }, 15000);
54
- afterAll(() => {
55
- mockServer?.kill();
56
- });
57
- describe('MCP Server Integration', () => {
58
- it('lists 8 tools', async () => {
59
- const mcpServer = spawn('node', [resolve(ROOT, 'dist/index.js')], {
60
- env: { ...process.env, MODA_API_KEY: 'moda_sk_test', MODA_BASE_URL: 'http://localhost:3002' },
61
- stdio: ['pipe', 'pipe', 'pipe'],
62
- });
63
- try {
64
- // Wait for server to start
65
- await new Promise((resolve, reject) => {
66
- const timeout = setTimeout(() => reject(new Error('MCP server did not start in time')), 10000);
67
- mcpServer.stderr.on('data', (data) => {
68
- if (data.toString().includes('running on stdio')) {
69
- clearTimeout(timeout);
70
- resolve();
71
- }
72
- });
73
- });
74
- const response = await sendJsonRpc(mcpServer, 'tools/list');
75
- expect(response.result.tools).toHaveLength(8);
76
- const toolNames = response.result.tools.map((t) => t.name);
77
- expect(toolNames).toContain('moda_get_overview');
78
- expect(toolNames).toContain('moda_get_frustrations');
79
- expect(toolNames).toContain('moda_get_tool_failures');
80
- }
81
- finally {
82
- mcpServer.kill();
83
- }
84
- }, 20000);
85
- it('calls moda_get_overview and returns data', async () => {
86
- const mcpServer = spawn('node', [resolve(ROOT, 'dist/index.js')], {
87
- env: { ...process.env, MODA_API_KEY: 'moda_sk_test', MODA_BASE_URL: 'http://localhost:3002' },
88
- stdio: ['pipe', 'pipe', 'pipe'],
89
- });
90
- try {
91
- await new Promise((resolve, reject) => {
92
- const timeout = setTimeout(() => reject(new Error('MCP server did not start in time')), 10000);
93
- mcpServer.stderr.on('data', (data) => {
94
- if (data.toString().includes('running on stdio')) {
95
- clearTimeout(timeout);
96
- resolve();
97
- }
98
- });
99
- });
100
- const response = await sendJsonRpc(mcpServer, 'tools/call', {
101
- name: 'moda_get_overview',
102
- arguments: { days_back: 7 },
103
- });
104
- expect(response.result.content).toHaveLength(1);
105
- expect(response.result.content[0].type).toBe('text');
106
- const data = JSON.parse(response.result.content[0].text);
107
- expect(data.conversations.total).toBe(1011);
108
- }
109
- finally {
110
- mcpServer.kill();
111
- }
112
- }, 20000);
113
- });
114
- //# sourceMappingURL=integration.test.js.map
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=schemas.test.d.ts.map