cucumberstudio-mcp 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.
- package/.env.example +36 -0
- package/.github/workflows/pr-checks.yml +41 -0
- package/.github/workflows/release.yml +194 -0
- package/.prettierignore +26 -0
- package/.prettierrc +14 -0
- package/CLAUDE.md +140 -0
- package/Dockerfile +50 -0
- package/Dockerfile.dev +31 -0
- package/LICENSE +21 -0
- package/README.md +395 -0
- package/build/api/client.d.ts +49 -0
- package/build/api/client.d.ts.map +1 -0
- package/build/api/client.js +204 -0
- package/build/api/client.js.map +1 -0
- package/build/api/types.d.ts +113 -0
- package/build/api/types.d.ts.map +1 -0
- package/build/api/types.js +2 -0
- package/build/api/types.js.map +1 -0
- package/build/config/settings.d.ts +123 -0
- package/build/config/settings.d.ts.map +1 -0
- package/build/config/settings.js +97 -0
- package/build/config/settings.js.map +1 -0
- package/build/constants.d.ts +16 -0
- package/build/constants.d.ts.map +1 -0
- package/build/constants.js +24 -0
- package/build/constants.js.map +1 -0
- package/build/generated/version.d.ts +3 -0
- package/build/generated/version.d.ts.map +1 -0
- package/build/generated/version.js +5 -0
- package/build/generated/version.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +81 -0
- package/build/index.js.map +1 -0
- package/build/mcp-server.d.ts +6 -0
- package/build/mcp-server.d.ts.map +1 -0
- package/build/mcp-server.js +263 -0
- package/build/mcp-server.js.map +1 -0
- package/build/tools/action-words.d.ts +18 -0
- package/build/tools/action-words.d.ts.map +1 -0
- package/build/tools/action-words.js +191 -0
- package/build/tools/action-words.js.map +1 -0
- package/build/tools/projects.d.ts +19 -0
- package/build/tools/projects.d.ts.map +1 -0
- package/build/tools/projects.js +123 -0
- package/build/tools/projects.js.map +1 -0
- package/build/tools/scenarios.d.ts +18 -0
- package/build/tools/scenarios.d.ts.map +1 -0
- package/build/tools/scenarios.js +194 -0
- package/build/tools/scenarios.js.map +1 -0
- package/build/tools/test-runs.d.ts +21 -0
- package/build/tools/test-runs.d.ts.map +1 -0
- package/build/tools/test-runs.js +324 -0
- package/build/tools/test-runs.js.map +1 -0
- package/build/transports/http.d.ts +38 -0
- package/build/transports/http.d.ts.map +1 -0
- package/build/transports/http.js +381 -0
- package/build/transports/http.js.map +1 -0
- package/build/transports/index.d.ts +22 -0
- package/build/transports/index.d.ts.map +1 -0
- package/build/transports/index.js +10 -0
- package/build/transports/index.js.map +1 -0
- package/build/transports/stdio.d.ts +13 -0
- package/build/transports/stdio.d.ts.map +1 -0
- package/build/transports/stdio.js +24 -0
- package/build/transports/stdio.js.map +1 -0
- package/build/utils/errors.d.ts +10 -0
- package/build/utils/errors.d.ts.map +1 -0
- package/build/utils/errors.js +35 -0
- package/build/utils/errors.js.map +1 -0
- package/build/utils/logger-constants.d.ts +15 -0
- package/build/utils/logger-constants.d.ts.map +1 -0
- package/build/utils/logger-constants.js +16 -0
- package/build/utils/logger-constants.js.map +1 -0
- package/build/utils/logger.d.ts +55 -0
- package/build/utils/logger.d.ts.map +1 -0
- package/build/utils/logger.js +113 -0
- package/build/utils/logger.js.map +1 -0
- package/build/utils/validation.d.ts +89 -0
- package/build/utils/validation.d.ts.map +1 -0
- package/build/utils/validation.js +78 -0
- package/build/utils/validation.js.map +1 -0
- package/docker-compose.yml +20 -0
- package/eslint.config.js +97 -0
- package/package.json +92 -0
- package/scripts/generate-version.js +31 -0
- package/src/api/client.ts +286 -0
- package/src/api/types.ts +137 -0
- package/src/config/settings.ts +113 -0
- package/src/constants.ts +29 -0
- package/src/index.ts +99 -0
- package/src/mcp-server.ts +342 -0
- package/src/tools/action-words.ts +240 -0
- package/src/tools/projects.ts +144 -0
- package/src/tools/scenarios.ts +231 -0
- package/src/tools/test-runs.ts +400 -0
- package/src/transports/http.ts +467 -0
- package/src/transports/index.ts +26 -0
- package/src/transports/stdio.ts +28 -0
- package/src/utils/errors.ts +45 -0
- package/src/utils/logger-constants.ts +18 -0
- package/src/utils/logger.ts +150 -0
- package/src/utils/validation.ts +94 -0
- package/test/api/client-with-msw.test.ts +122 -0
- package/test/api/client.test.ts +326 -0
- package/test/api/types.test.ts +88 -0
- package/test/config/settings.test.ts +204 -0
- package/test/mocks/data/action-words.ts +40 -0
- package/test/mocks/data/index.ts +13 -0
- package/test/mocks/data/projects.ts +38 -0
- package/test/mocks/data/scenarios.ts +53 -0
- package/test/mocks/data/test-runs.ts +101 -0
- package/test/mocks/handlers/action-words.ts +52 -0
- package/test/mocks/handlers/index.ts +10 -0
- package/test/mocks/handlers/projects.ts +45 -0
- package/test/mocks/handlers/scenarios.ts +72 -0
- package/test/mocks/handlers/test-runs.ts +106 -0
- package/test/mocks/server.ts +26 -0
- package/test/setup/vitest.setup.ts +18 -0
- package/test/tools/coverage-boost.test.ts +252 -0
- package/test/tools/projects.test.ts +290 -0
- package/test/tools/tools-basic.test.ts +146 -0
- package/test/transports/http-basic.test.ts +87 -0
- package/test/transports/http-simple.test.ts +33 -0
- package/test/transports/stdio.test.ts +73 -0
- package/test/utils/errors.test.ts +117 -0
- package/test/utils/validation.test.ts +261 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +27 -0
- package/vitest.config.ts +43 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { CallToolRequest, TextContent } from '@modelcontextprotocol/sdk/types.js'
|
|
2
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { CucumberStudioApiClient } from '../../src/api/client.js'
|
|
5
|
+
import { ProjectTools } from '../../src/tools/projects.js'
|
|
6
|
+
|
|
7
|
+
// Mock the API client
|
|
8
|
+
vi.mock('../../src/api/client.js')
|
|
9
|
+
|
|
10
|
+
describe('ProjectTools', () => {
|
|
11
|
+
let mockApiClient: any
|
|
12
|
+
let projectTools: ProjectTools
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockApiClient = {
|
|
16
|
+
getProjects: vi.fn(),
|
|
17
|
+
getProject: vi.fn(),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
projectTools = new ProjectTools(mockApiClient as any)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('getTools', () => {
|
|
24
|
+
it('should return array of available tools', () => {
|
|
25
|
+
const tools = projectTools.getTools()
|
|
26
|
+
|
|
27
|
+
expect(tools).toHaveLength(2)
|
|
28
|
+
expect(tools[0].name).toBe('cucumberstudio_list_projects')
|
|
29
|
+
expect(tools[1].name).toBe('cucumberstudio_get_project')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should include proper tool schemas', () => {
|
|
33
|
+
const tools = projectTools.getTools()
|
|
34
|
+
|
|
35
|
+
const listTool = tools.find((t) => t.name === 'cucumberstudio_list_projects')
|
|
36
|
+
expect(listTool?.description).toContain('List all projects')
|
|
37
|
+
expect(listTool?.inputSchema.type).toBe('object')
|
|
38
|
+
expect(listTool?.inputSchema.additionalProperties).toBe(false)
|
|
39
|
+
|
|
40
|
+
const getTool = tools.find((t) => t.name === 'cucumberstudio_get_project')
|
|
41
|
+
expect(getTool?.description).toContain('Get detailed information')
|
|
42
|
+
expect(getTool?.inputSchema.required).toContain('projectId')
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('handleToolCall', () => {
|
|
47
|
+
describe('cucumberstudio_list_projects', () => {
|
|
48
|
+
it('should handle list projects request without parameters', async () => {
|
|
49
|
+
const mockResponse = {
|
|
50
|
+
data: [
|
|
51
|
+
{
|
|
52
|
+
id: '1',
|
|
53
|
+
attributes: {
|
|
54
|
+
name: 'Test Project',
|
|
55
|
+
description: 'A test project',
|
|
56
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
57
|
+
updated_at: '2023-01-02T00:00:00Z',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
meta: { total_count: 1 },
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
mockApiClient.getProjects.mockResolvedValue(mockResponse)
|
|
65
|
+
|
|
66
|
+
const request: CallToolRequest = {
|
|
67
|
+
method: 'tools/call',
|
|
68
|
+
params: {
|
|
69
|
+
name: 'cucumberstudio_list_projects',
|
|
70
|
+
arguments: {},
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = await projectTools.handleToolCall(request)
|
|
75
|
+
|
|
76
|
+
expect(mockApiClient.getProjects).toHaveBeenCalledWith({})
|
|
77
|
+
expect(result.content).toHaveLength(1)
|
|
78
|
+
|
|
79
|
+
const content = result.content[0] as TextContent
|
|
80
|
+
const responseData = JSON.parse(content.text)
|
|
81
|
+
|
|
82
|
+
expect(responseData.projects).toHaveLength(1)
|
|
83
|
+
expect(responseData.projects[0]).toEqual({
|
|
84
|
+
id: '1',
|
|
85
|
+
name: 'Test Project',
|
|
86
|
+
description: 'A test project',
|
|
87
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
88
|
+
updated_at: '2023-01-02T00:00:00Z',
|
|
89
|
+
})
|
|
90
|
+
expect(responseData.meta).toEqual({ total_count: 1 })
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should handle list projects request with pagination', async () => {
|
|
94
|
+
const mockResponse = {
|
|
95
|
+
data: [],
|
|
96
|
+
meta: { total_count: 0 },
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
mockApiClient.getProjects.mockResolvedValue(mockResponse)
|
|
100
|
+
|
|
101
|
+
const request: CallToolRequest = {
|
|
102
|
+
method: 'tools/call',
|
|
103
|
+
params: {
|
|
104
|
+
name: 'cucumberstudio_list_projects',
|
|
105
|
+
arguments: {
|
|
106
|
+
pagination: { page: 2, pageSize: 10 },
|
|
107
|
+
filter: { name: 'test' },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await projectTools.handleToolCall(request)
|
|
113
|
+
|
|
114
|
+
expect(mockApiClient.getProjects).toHaveBeenCalledWith({
|
|
115
|
+
'page[number]': 2,
|
|
116
|
+
'page[size]': 10,
|
|
117
|
+
'filter[name]': 'test',
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should handle single project data format', async () => {
|
|
122
|
+
const mockResponse = {
|
|
123
|
+
data: {
|
|
124
|
+
id: '1',
|
|
125
|
+
attributes: {
|
|
126
|
+
name: 'Single Project',
|
|
127
|
+
description: 'Single project response',
|
|
128
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
129
|
+
updated_at: '2023-01-02T00:00:00Z',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
mockApiClient.getProjects.mockResolvedValue(mockResponse)
|
|
135
|
+
|
|
136
|
+
const request: CallToolRequest = {
|
|
137
|
+
method: 'tools/call',
|
|
138
|
+
params: {
|
|
139
|
+
name: 'cucumberstudio_list_projects',
|
|
140
|
+
arguments: {},
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const result = await projectTools.handleToolCall(request)
|
|
145
|
+
const content = result.content[0] as TextContent
|
|
146
|
+
const responseData = JSON.parse(content.text)
|
|
147
|
+
|
|
148
|
+
expect(responseData.projects).toHaveLength(1)
|
|
149
|
+
expect(responseData.projects[0].name).toBe('Single Project')
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('cucumberstudio_get_project', () => {
|
|
154
|
+
it('should handle get project request', async () => {
|
|
155
|
+
const mockResponse = {
|
|
156
|
+
data: {
|
|
157
|
+
id: '123',
|
|
158
|
+
type: 'projects',
|
|
159
|
+
attributes: {
|
|
160
|
+
name: 'Specific Project',
|
|
161
|
+
description: 'Project details',
|
|
162
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
163
|
+
updated_at: '2023-01-02T00:00:00Z',
|
|
164
|
+
},
|
|
165
|
+
relationships: {
|
|
166
|
+
scenarios: { data: [] },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
included: [],
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
mockApiClient.getProject.mockResolvedValue(mockResponse)
|
|
173
|
+
|
|
174
|
+
const request: CallToolRequest = {
|
|
175
|
+
method: 'tools/call',
|
|
176
|
+
params: {
|
|
177
|
+
name: 'cucumberstudio_get_project',
|
|
178
|
+
arguments: { projectId: '123' },
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const result = await projectTools.handleToolCall(request)
|
|
183
|
+
|
|
184
|
+
expect(mockApiClient.getProject).toHaveBeenCalledWith('123')
|
|
185
|
+
|
|
186
|
+
const content = result.content[0] as TextContent
|
|
187
|
+
const responseData = JSON.parse(content.text)
|
|
188
|
+
|
|
189
|
+
expect(responseData.project).toEqual({
|
|
190
|
+
id: '123',
|
|
191
|
+
type: 'projects',
|
|
192
|
+
name: 'Specific Project',
|
|
193
|
+
description: 'Project details',
|
|
194
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
195
|
+
updated_at: '2023-01-02T00:00:00Z',
|
|
196
|
+
relationships: { scenarios: { data: [] } },
|
|
197
|
+
})
|
|
198
|
+
expect(responseData.included).toEqual([])
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('should validate required projectId parameter', async () => {
|
|
202
|
+
const request: CallToolRequest = {
|
|
203
|
+
method: 'tools/call',
|
|
204
|
+
params: {
|
|
205
|
+
name: 'cucumberstudio_get_project',
|
|
206
|
+
arguments: {},
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await expect(projectTools.handleToolCall(request)).rejects.toThrow()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should validate empty projectId', async () => {
|
|
214
|
+
const request: CallToolRequest = {
|
|
215
|
+
method: 'tools/call',
|
|
216
|
+
params: {
|
|
217
|
+
name: 'cucumberstudio_get_project',
|
|
218
|
+
arguments: { projectId: '' },
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await expect(projectTools.handleToolCall(request)).rejects.toThrow()
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('should throw error for unknown tool', async () => {
|
|
227
|
+
const request: CallToolRequest = {
|
|
228
|
+
method: 'tools/call',
|
|
229
|
+
params: {
|
|
230
|
+
name: 'unknown_tool',
|
|
231
|
+
arguments: {},
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await expect(projectTools.handleToolCall(request)).rejects.toThrow('Unknown tool: unknown_tool')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('should handle API errors gracefully', async () => {
|
|
239
|
+
mockApiClient.getProjects.mockRejectedValue(new Error('API Error'))
|
|
240
|
+
|
|
241
|
+
const request: CallToolRequest = {
|
|
242
|
+
method: 'tools/call',
|
|
243
|
+
params: {
|
|
244
|
+
name: 'cucumberstudio_list_projects',
|
|
245
|
+
arguments: {},
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await expect(projectTools.handleToolCall(request)).rejects.toThrow()
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
describe('error handling', () => {
|
|
254
|
+
it('should handle missing project attributes', async () => {
|
|
255
|
+
const mockResponse = {
|
|
256
|
+
data: [
|
|
257
|
+
{
|
|
258
|
+
id: '1',
|
|
259
|
+
type: 'projects',
|
|
260
|
+
attributes: {
|
|
261
|
+
// Missing name field to test fallback
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
mockApiClient.getProjects.mockResolvedValue(mockResponse)
|
|
268
|
+
|
|
269
|
+
const request: CallToolRequest = {
|
|
270
|
+
method: 'tools/call',
|
|
271
|
+
params: {
|
|
272
|
+
name: 'cucumberstudio_list_projects',
|
|
273
|
+
arguments: {},
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const result = await projectTools.handleToolCall(request)
|
|
278
|
+
const content = result.content[0] as TextContent
|
|
279
|
+
const responseData = JSON.parse(content.text)
|
|
280
|
+
|
|
281
|
+
expect(responseData.projects[0]).toEqual({
|
|
282
|
+
id: '1',
|
|
283
|
+
name: 'Unknown',
|
|
284
|
+
description: '',
|
|
285
|
+
created_at: undefined,
|
|
286
|
+
updated_at: undefined,
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
})
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// import { CucumberStudioApiClient } from '@/api/client.js'
|
|
4
|
+
import { ActionWordTools } from '@/tools/action-words.js'
|
|
5
|
+
import { ScenarioTools } from '@/tools/scenarios.js'
|
|
6
|
+
import { TestRunTools } from '@/tools/test-runs.js'
|
|
7
|
+
|
|
8
|
+
// Mock the API client
|
|
9
|
+
vi.mock('@/api/client.js')
|
|
10
|
+
|
|
11
|
+
describe('Tools Basic Functionality', () => {
|
|
12
|
+
let mockApiClient: any
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockApiClient = {
|
|
16
|
+
getProjects: vi.fn(),
|
|
17
|
+
getProject: vi.fn(),
|
|
18
|
+
getScenarios: vi.fn(),
|
|
19
|
+
getScenario: vi.fn(),
|
|
20
|
+
getFolders: vi.fn(),
|
|
21
|
+
getActionWords: vi.fn(),
|
|
22
|
+
getActionWord: vi.fn(),
|
|
23
|
+
getTestRuns: vi.fn(),
|
|
24
|
+
getTestRun: vi.fn(),
|
|
25
|
+
getTestExecutions: vi.fn(),
|
|
26
|
+
getBuilds: vi.fn(),
|
|
27
|
+
getBuild: vi.fn(),
|
|
28
|
+
getExecutionEnvironments: vi.fn(),
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('ScenarioTools', () => {
|
|
33
|
+
it('should instantiate and provide tools', () => {
|
|
34
|
+
const scenarioTools = new ScenarioTools(mockApiClient)
|
|
35
|
+
const tools = scenarioTools.getTools()
|
|
36
|
+
|
|
37
|
+
expect(tools).toBeDefined()
|
|
38
|
+
expect(Array.isArray(tools)).toBe(true)
|
|
39
|
+
expect(tools.length).toBeGreaterThan(0)
|
|
40
|
+
|
|
41
|
+
// Check that all tools have required properties
|
|
42
|
+
tools.forEach((tool) => {
|
|
43
|
+
expect(tool.name).toBeDefined()
|
|
44
|
+
expect(tool.description).toBeDefined()
|
|
45
|
+
expect(tool.inputSchema).toBeDefined()
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should have scenario-related tool names', () => {
|
|
50
|
+
const scenarioTools = new ScenarioTools(mockApiClient)
|
|
51
|
+
const tools = scenarioTools.getTools()
|
|
52
|
+
const toolNames = tools.map((tool) => tool.name)
|
|
53
|
+
|
|
54
|
+
expect(toolNames.some((name) => name.includes('scenario'))).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should throw error for unknown tool', async () => {
|
|
58
|
+
const scenarioTools = new ScenarioTools(mockApiClient)
|
|
59
|
+
|
|
60
|
+
const request = {
|
|
61
|
+
method: 'tools/call' as const,
|
|
62
|
+
params: {
|
|
63
|
+
name: 'unknown_tool',
|
|
64
|
+
arguments: {},
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
await expect(scenarioTools.handleToolCall(request)).rejects.toThrow()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('ActionWordTools', () => {
|
|
72
|
+
it('should instantiate and provide tools', () => {
|
|
73
|
+
const actionWordTools = new ActionWordTools(mockApiClient)
|
|
74
|
+
const tools = actionWordTools.getTools()
|
|
75
|
+
|
|
76
|
+
expect(tools).toBeDefined()
|
|
77
|
+
expect(Array.isArray(tools)).toBe(true)
|
|
78
|
+
expect(tools.length).toBeGreaterThan(0)
|
|
79
|
+
|
|
80
|
+
tools.forEach((tool) => {
|
|
81
|
+
expect(tool.name).toBeDefined()
|
|
82
|
+
expect(tool.description).toBeDefined()
|
|
83
|
+
expect(tool.inputSchema).toBeDefined()
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should have action word related tool names', () => {
|
|
88
|
+
const actionWordTools = new ActionWordTools(mockApiClient)
|
|
89
|
+
const tools = actionWordTools.getTools()
|
|
90
|
+
const toolNames = tools.map((tool) => tool.name)
|
|
91
|
+
|
|
92
|
+
expect(toolNames.some((name) => name.includes('action_word'))).toBe(true)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should throw error for unknown tool', async () => {
|
|
96
|
+
const actionWordTools = new ActionWordTools(mockApiClient)
|
|
97
|
+
|
|
98
|
+
const request = {
|
|
99
|
+
method: 'tools/call' as const,
|
|
100
|
+
params: {
|
|
101
|
+
name: 'unknown_tool',
|
|
102
|
+
arguments: {},
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
await expect(actionWordTools.handleToolCall(request)).rejects.toThrow()
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('TestRunTools', () => {
|
|
110
|
+
it('should instantiate and provide tools', () => {
|
|
111
|
+
const testRunTools = new TestRunTools(mockApiClient)
|
|
112
|
+
const tools = testRunTools.getTools()
|
|
113
|
+
|
|
114
|
+
expect(tools).toBeDefined()
|
|
115
|
+
expect(Array.isArray(tools)).toBe(true)
|
|
116
|
+
expect(tools.length).toBeGreaterThan(0)
|
|
117
|
+
|
|
118
|
+
tools.forEach((tool) => {
|
|
119
|
+
expect(tool.name).toBeDefined()
|
|
120
|
+
expect(tool.description).toBeDefined()
|
|
121
|
+
expect(tool.inputSchema).toBeDefined()
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should have test run related tool names', () => {
|
|
126
|
+
const testRunTools = new TestRunTools(mockApiClient)
|
|
127
|
+
const tools = testRunTools.getTools()
|
|
128
|
+
const toolNames = tools.map((tool) => tool.name)
|
|
129
|
+
|
|
130
|
+
expect(toolNames.some((name) => name.includes('test_run') || name.includes('build'))).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should throw error for unknown tool', async () => {
|
|
134
|
+
const testRunTools = new TestRunTools(mockApiClient)
|
|
135
|
+
|
|
136
|
+
const request = {
|
|
137
|
+
method: 'tools/call' as const,
|
|
138
|
+
params: {
|
|
139
|
+
name: 'unknown_tool',
|
|
140
|
+
arguments: {},
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
await expect(testRunTools.handleToolCall(request)).rejects.toThrow()
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
})
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { StreamableHttpTransport } from '@/transports/http.js'
|
|
4
|
+
import { NoOpLogger } from '@/utils/logger.js'
|
|
5
|
+
|
|
6
|
+
describe('StreamableHttpTransport Basic', () => {
|
|
7
|
+
describe('constructor', () => {
|
|
8
|
+
it('should create transport with default options', () => {
|
|
9
|
+
const mockCreateServer = vi.fn()
|
|
10
|
+
const logger = new NoOpLogger()
|
|
11
|
+
const transport = new StreamableHttpTransport(
|
|
12
|
+
mockCreateServer,
|
|
13
|
+
{
|
|
14
|
+
port: 3000,
|
|
15
|
+
host: '127.0.0.1',
|
|
16
|
+
cors: { origin: true },
|
|
17
|
+
},
|
|
18
|
+
logger,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
expect(transport).toBeDefined()
|
|
22
|
+
expect(typeof transport.start).toBe('function')
|
|
23
|
+
expect(typeof transport.close).toBe('function')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should handle custom CORS options', () => {
|
|
27
|
+
const mockCreateServer = vi.fn()
|
|
28
|
+
const logger = new NoOpLogger()
|
|
29
|
+
const transport = new StreamableHttpTransport(
|
|
30
|
+
mockCreateServer,
|
|
31
|
+
{
|
|
32
|
+
port: 3001,
|
|
33
|
+
host: 'localhost',
|
|
34
|
+
cors: {
|
|
35
|
+
origin: ['http://localhost:3000'],
|
|
36
|
+
credentials: true,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
logger,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
expect(transport).toBeDefined()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('origin validation', () => {
|
|
47
|
+
it('should validate localhost origins', () => {
|
|
48
|
+
const mockCreateServer = vi.fn()
|
|
49
|
+
const logger = new NoOpLogger()
|
|
50
|
+
const transport = new StreamableHttpTransport(
|
|
51
|
+
mockCreateServer,
|
|
52
|
+
{
|
|
53
|
+
port: 3000,
|
|
54
|
+
host: '127.0.0.1',
|
|
55
|
+
cors: { origin: true },
|
|
56
|
+
},
|
|
57
|
+
logger,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// Test that the transport can be instantiated with localhost origins
|
|
61
|
+
expect(transport).toBeDefined()
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('server management', () => {
|
|
66
|
+
it('should handle server lifecycle methods', async () => {
|
|
67
|
+
const mockCreateServer = vi.fn()
|
|
68
|
+
const logger = new NoOpLogger()
|
|
69
|
+
const transport = new StreamableHttpTransport(
|
|
70
|
+
mockCreateServer,
|
|
71
|
+
{
|
|
72
|
+
port: 0, // Use random port to avoid conflicts
|
|
73
|
+
host: '127.0.0.1',
|
|
74
|
+
cors: { origin: true },
|
|
75
|
+
},
|
|
76
|
+
logger,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// Test that lifecycle methods exist and can be called
|
|
80
|
+
expect(typeof transport.start).toBe('function')
|
|
81
|
+
expect(typeof transport.close).toBe('function')
|
|
82
|
+
|
|
83
|
+
// For now, just test that close doesn't throw when called without starting
|
|
84
|
+
await expect(transport.close()).resolves.not.toThrow()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// Simple HTTP transport test without complex express mocking
|
|
4
|
+
describe('StreamableHttpTransport (Simple)', () => {
|
|
5
|
+
it('should validate that HTTP transport class can be imported', async () => {
|
|
6
|
+
const { StreamableHttpTransport } = await import('@/transports/http.js')
|
|
7
|
+
expect(StreamableHttpTransport).toBeDefined()
|
|
8
|
+
expect(typeof StreamableHttpTransport).toBe('function')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should validate HttpTransportOptions interface', () => {
|
|
12
|
+
// Test that we can create valid options
|
|
13
|
+
const options = {
|
|
14
|
+
port: 3000,
|
|
15
|
+
host: '127.0.0.1',
|
|
16
|
+
cors: {
|
|
17
|
+
origin: true,
|
|
18
|
+
credentials: true,
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
expect(options.port).toBe(3000)
|
|
23
|
+
expect(options.host).toBe('127.0.0.1')
|
|
24
|
+
expect(options.cors.origin).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should test origin validation helper functions', async () => {
|
|
28
|
+
// We can test any exported utility functions here
|
|
29
|
+
// For now, just verify the module loads properly
|
|
30
|
+
const httpModule = await import('@/transports/http.js')
|
|
31
|
+
expect(httpModule).toBeDefined()
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { StdioTransport } from '../../src/transports/stdio.js'
|
|
6
|
+
|
|
7
|
+
// Mock the SDK components
|
|
8
|
+
vi.mock('@modelcontextprotocol/sdk/server/index.js')
|
|
9
|
+
vi.mock('@modelcontextprotocol/sdk/server/stdio.js')
|
|
10
|
+
|
|
11
|
+
describe('StdioTransport', () => {
|
|
12
|
+
let mockServer: any
|
|
13
|
+
let mockStdioTransport: any
|
|
14
|
+
let transport: StdioTransport
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockServer = {
|
|
18
|
+
connect: vi.fn(),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
mockStdioTransport = {
|
|
22
|
+
close: vi.fn(),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
vi.mocked(StdioServerTransport).mockImplementation(() => mockStdioTransport)
|
|
26
|
+
|
|
27
|
+
transport = new StdioTransport(mockServer)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('constructor', () => {
|
|
31
|
+
it('should create StdioServerTransport instance', () => {
|
|
32
|
+
expect(StdioServerTransport).toHaveBeenCalled()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should store server reference', () => {
|
|
36
|
+
expect(transport).toBeDefined()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('start', () => {
|
|
41
|
+
it('should connect server to transport', async () => {
|
|
42
|
+
mockServer.connect.mockResolvedValue(undefined)
|
|
43
|
+
|
|
44
|
+
await transport.start()
|
|
45
|
+
|
|
46
|
+
expect(mockServer.connect).toHaveBeenCalledWith(mockStdioTransport)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should handle connection errors', async () => {
|
|
50
|
+
const error = new Error('Connection failed')
|
|
51
|
+
mockServer.connect.mockRejectedValue(error)
|
|
52
|
+
|
|
53
|
+
await expect(transport.start()).rejects.toThrow('Connection failed')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('close', () => {
|
|
58
|
+
it('should close the transport', async () => {
|
|
59
|
+
mockStdioTransport.close.mockResolvedValue(undefined)
|
|
60
|
+
|
|
61
|
+
await transport.close()
|
|
62
|
+
|
|
63
|
+
expect(mockStdioTransport.close).toHaveBeenCalled()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should handle close errors gracefully', async () => {
|
|
67
|
+
const error = new Error('Close failed')
|
|
68
|
+
mockStdioTransport.close.mockRejectedValue(error)
|
|
69
|
+
|
|
70
|
+
await expect(transport.close()).rejects.toThrow('Close failed')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
})
|