Skip to content

Commit 96dbac5

Browse files
author
DavertMik
committed
Basic MCP implementation
1 parent 9a9442a commit 96dbac5

File tree

3 files changed

+230
-3
lines changed

3 files changed

+230
-3
lines changed

bin/codecept-mcp.js

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/usr/bin/env node
2+
3+
const { McpServer, ResourceTemplate } = require('@modelcontextprotocol/sdk/server/mcp.js')
4+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js')
5+
const path = require('path')
6+
const fs = require('fs')
7+
8+
// Import core CodeceptJS modules
9+
const Codecept = require('../lib/codecept')
10+
const container = require('../lib/container')
11+
const { getParamsToString } = require('../lib/parser')
12+
const { methodsOfObject } = require('../lib/utils')
13+
const output = require('../lib/output')
14+
const { getConfig, getTestRoot } = require('../lib/command/utils')
15+
16+
// Simple path handling - use argument if provided, otherwise use current directory
17+
18+
const lastArg = process.argv[process.argv.length - 1]
19+
20+
const customPath = lastArg.includes('mcp.js') ? process.cwd() : lastArg
21+
22+
/**
23+
* Start MCP Server
24+
*/
25+
async function startServer() {
26+
// Disable default output
27+
output.print = () => {}
28+
29+
// Initialize CodeceptJS
30+
const testsPath = getTestRoot(customPath)
31+
const config = getConfig(customPath)
32+
const codecept = new Codecept(config, {})
33+
34+
codecept.init(testsPath)
35+
codecept.loadTests()
36+
37+
// Setup MCP server
38+
const server = new McpServer({
39+
name: 'CodeceptJS',
40+
version: '1.0.0',
41+
url: 'https://codecept.io',
42+
description: 'CodeceptJS Model Context Protocol Server',
43+
})
44+
45+
// Convert Resource: tests
46+
server.tool('list-tests', {}, async () => {
47+
// Use the same approach as dryRun.js to collect test information
48+
const mocha = container.mocha()
49+
mocha.files = codecept.testFiles
50+
mocha.loadFiles()
51+
52+
const tests = []
53+
54+
// Iterate through all suites and tests
55+
for (const suite of mocha.suite.suites) {
56+
for (const test of suite.tests) {
57+
tests.push({
58+
title: test.title,
59+
fullTitle: test.fullTitle(),
60+
body: test.body ? test.body.toString() : '',
61+
file: suite.file,
62+
suiteName: suite.title,
63+
meta: test.meta,
64+
tags: test.tags,
65+
})
66+
}
67+
}
68+
69+
// Format each test as a readable text block
70+
const formattedText = tests
71+
.map(test => {
72+
return [`Test: ${test.fullTitle}`, `File: ${test.file}`, `Suite: ${test.suiteName}`, test.tags && test.tags.length ? `Tags: ${test.tags?.join(', ')}` : '', '', 'Body:', test.body, '---']
73+
.filter(Boolean)
74+
.join('\n')
75+
})
76+
.join('\n\n')
77+
78+
return {
79+
content: [{ type: 'text', text: formattedText }],
80+
}
81+
})
82+
83+
// Convert Resource: suites
84+
server.tool('list-suites', {}, async () => {
85+
// Use the same approach as dryRun.js to collect suite information
86+
const mocha = container.mocha()
87+
mocha.files = codecept.testFiles
88+
mocha.loadFiles()
89+
90+
const suites = []
91+
92+
// Iterate through all suites
93+
for (const suite of mocha.suite.suites) {
94+
suites.push({
95+
title: suite.title,
96+
file: suite.file,
97+
testCount: suite.tests.length,
98+
tests: suite.tests.map(test => ({
99+
title: test.title,
100+
fullTitle: test.fullTitle(),
101+
})),
102+
})
103+
}
104+
105+
// Format each suite as a readable text block
106+
const formattedText = suites
107+
.map(suite => {
108+
const testList = suite.tests.map(test => ` - ${test.title}`).join('\n')
109+
return [`Suite: ${suite.title}`, `File: ${suite.file}`, `Tests (${suite.testCount}):`, testList, '---'].join('\n')
110+
})
111+
.join('\n\n')
112+
113+
return {
114+
content: [{ type: 'text', text: formattedText }],
115+
}
116+
})
117+
118+
// // Convert Resource: actions
119+
// server.tool("list-actions",
120+
// { },
121+
// async () => {
122+
// const helpers = container.helpers();
123+
// const supportI = container.support('I');
124+
// const actions = [];
125+
126+
// // Get actions from helpers
127+
// for (const name in helpers) {
128+
// const helper = helpers[name];
129+
// methodsOfObject(helper).forEach(action => {
130+
// const params = getParamsToString(helper[action]);
131+
// actions.push({
132+
// name: action,
133+
// source: name,
134+
// params,
135+
// type: 'helper'
136+
// });
137+
// });
138+
// }
139+
140+
// // Get actions from I
141+
// for (const name in supportI) {
142+
// if (actions.some(a => a.name === name)) {
143+
// continue;
144+
// }
145+
// const actor = supportI[name];
146+
// const params = getParamsToString(actor);
147+
// actions.push({
148+
// name,
149+
// source: 'I',
150+
// params,
151+
// type: 'support'
152+
// });
153+
// }
154+
155+
// // Format actions as a readable text list
156+
// const helperActions = actions.filter(a => a.type === 'helper')
157+
// .sort((a, b) => a.source === b.source ? a.name.localeCompare(b.name) : a.source.localeCompare(b.source));
158+
159+
// const supportActions = actions.filter(a => a.type === 'support')
160+
// .sort((a, b) => a.name.localeCompare(b.name));
161+
162+
// // Create formatted text output
163+
// const formattedText = [
164+
// '# Helper Actions',
165+
// ...helperActions.map(a => `${a.source}.${a.name}(${a.params})`),
166+
// '',
167+
// '# Support Actions',
168+
// ...supportActions.map(a => `I.${a.name}(${a.params})`)
169+
// ].join('\n');
170+
171+
// return {
172+
// content: [{ type: "text", text: formattedText }]
173+
// };
174+
// }
175+
// );
176+
// Start MCP server using stdio transport
177+
const transport = new StdioServerTransport()
178+
await server.connect(transport)
179+
}
180+
181+
// Start the server without nested error handling
182+
startServer().catch(err => {
183+
console.error(err)
184+
process.exit(1)
185+
})

docs/MCP.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# CodeceptJS Model Context Protocol (MCP) Integration
2+
3+
CodeceptJS supports the Model Context Protocol (MCP) for IDE integration, particularly with Cursor.
4+
5+
## Using with Cursor
6+
7+
To use CodeceptJS with Cursor:
8+
9+
1. Start the MCP server directly using the standalone executable:
10+
11+
```bash
12+
npx codecept-mcp
13+
```
14+
15+
or
16+
17+
```bash
18+
node bin/codecept-mcp.js [path]
19+
```
20+
21+
Where `[path]` is optional and points to your CodeceptJS project (defaults to current directory).
22+
23+
2. In Cursor, connect to the MCP server by selecting "Connect to MCP Server" and choose "CodeceptJS" from the list.
24+
25+
## Available Tools
26+
27+
The server provides these tools:
28+
29+
- `list-tests`: List all availble tests
30+
- `list-suites`: List all suites
31+
32+
## Troubleshooting
33+
34+
If Cursor shows "Found 0 tools, 0 resources, and 0 resource templates" when connecting:
35+
36+
1. Make sure you're using the standalone executable (`codecept-mcp.js`)
37+
2. The MCP server process should be running independently and not as a subprocess
38+
3. Check that you're in the correct CodeceptJS project directory by specifying path as a parameter
39+
4. Verify that your CodeceptJS configuration file (codecept.conf.js or codecept.json) exists in path and is valid

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
},
3939
"types": "typings/index.d.ts",
4040
"bin": {
41-
"codeceptjs": "./bin/codecept.js"
41+
"codeceptjs": "./bin/codecept.js",
42+
"codecept-mcp": "./bin/codecept-mcp.js"
4243
},
4344
"repository": "Codeception/codeceptjs",
4445
"scripts": {
@@ -80,6 +81,7 @@
8081
"@cucumber/cucumber-expressions": "18",
8182
"@cucumber/gherkin": "32.1.0",
8283
"@cucumber/messages": "27.2.0",
84+
"@modelcontextprotocol/sdk": "^1.10.2",
8385
"@xmldom/xmldom": "0.9.8",
8486
"acorn": "8.14.1",
8587
"arrify": "3.0.0",
@@ -95,8 +97,8 @@
9597
"figures": "3.2.0",
9698
"fn-args": "4.0.0",
9799
"fs-extra": "11.3.0",
98-
"glob": ">=9.0.0 <12",
99100
"fuse.js": "^7.0.0",
101+
"glob": ">=9.0.0 <12",
100102
"html-minifier-terser": "7.2.0",
101103
"inquirer": "8.2.6",
102104
"invisi-data": "^1.0.0",
@@ -114,7 +116,8 @@
114116
"promise-retry": "1.1.1",
115117
"resq": "1.11.0",
116118
"sprintf-js": "1.1.3",
117-
"uuid": "11.1.0"
119+
"uuid": "11.1.0",
120+
"zod": "^3.24.3"
118121
},
119122
"optionalDependencies": {
120123
"@codeceptjs/detox-helper": "1.1.8"

0 commit comments

Comments
 (0)