Skip to content

Commit a643804

Browse files
DavertMikclaude
andcommitted
feat: add init:agent command to set up MCP and skills for coding agents
`npx codeceptjs init:agent [claude|codex|cursor|opencode]` registers the CodeceptJS MCP server and installs CodeceptJS skills for the chosen agent. Claude and Codex use their own `mcp add` commands; Cursor and OpenCode get their project MCP file merged. `init` gains `--agent` and a final prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ff47c22 commit a643804

7 files changed

Lines changed: 425 additions & 2 deletions

File tree

‎bin/codecept.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,17 @@ program
7878
.command('init [path]')
7979
.description('Creates dummy config in current dir or [path]')
8080
.option('-y, --yes', 'skip prompts and use defaults (Playwright + chromium, BASE_URL env for url)')
81+
.option('--agent <name>', 'set up an AI coding agent: claude, codex, cursor, opencode')
8182
.action(commandHandler('../lib/command/init.js'))
8283

84+
program
85+
.command('init:agent [agent]')
86+
.alias('init-agent')
87+
.description('Set up CodeceptJS MCP server and skills for a coding agent: claude, codex, cursor, opencode')
88+
.option(commandFlags.config.flag, commandFlags.config.description)
89+
.option('-y, --yes', 'skip prompts, requires [agent]')
90+
.action(commandHandler('../lib/command/initAgent.js'))
91+
8392
program
8493
.command('check')
8594
.option(commandFlags.config.flag, commandFlags.config.description)

‎docs/agents.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,15 @@ CodeceptJS is token-efficient: it stores HTML, ARIA, logs, and HTTP request data
2525

2626
## Essential Setup
2727

28-
Two things make agent testing work: the **skills** that teach the agent CodeceptJS, and the **MCP server** that lets it drive the browser. Set both up once, from your project directory.
28+
Two things make agent testing work: the **skills** that teach the agent CodeceptJS, and the **MCP server** that lets it drive the browser. Set both up once, from your project directory:
29+
30+
```bash
31+
npx codeceptjs init:agent claude
32+
```
33+
34+
Use `codex`, `cursor`, or `opencode` instead of `claude` for other agents, or omit the name to pick from a list. `npx codeceptjs init` offers the same step at the end.
35+
36+
### Manual setup
2937

3038
Install the skills:
3139

‎docs/commands.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,16 @@ Or in provided path
262262
npx codecept init test
263263
```
264264

265+
## Init Agent
266+
267+
Registers the CodeceptJS MCP server and installs CodeceptJS skills for a coding agent (`claude`, `codex`, `cursor`, `opencode`):
268+
269+
```sh
270+
npx codeceptjs init:agent claude
271+
```
272+
273+
Omit the agent to pick it from a list. Use `-c` to point the MCP server to a config other than `./codecept.conf.js`. See [Agentic Testing](/agents).
274+
265275
## Migrate
266276

267277
Migrate your current `codecept.json` to `codecept.conf.js`

‎docs/mcp.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ Two ways to launch the server:
2727
2828
## Configuration
2929

30-
Set up the MCP server in your client (Claude Desktop, Cursor, Continue, etc.):
30+
For Claude Code, Codex, Cursor, or OpenCode, one command registers the server and installs the CodeceptJS skills:
31+
32+
```bash
33+
npx codeceptjs init:agent claude
34+
```
35+
36+
To set up the MCP server in another client (Claude Desktop, Continue, etc.), add it to the client config:
3137

3238
### Basic
3339

‎lib/command/init.js‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { fileExists, beautify, installedLocally } from '../utils.js'
1212
import { getTestRoot } from './utils.js'
1313
import generateDefinitions from './definitions.js'
1414
import { test as generateTest } from './generate.js'
15+
import { agents, setupAgent } from './initAgent.js'
1516
const isLocal = installedLocally()
1617

1718
const defaultConfig = {
@@ -72,6 +73,11 @@ export default async function (initPath, options = {}) {
7273
const testsPath = getTestRoot(initPath)
7374
const skipPrompts = !!options.yes
7475

76+
if (options.agent && !agents[options.agent]) {
77+
error(`Unknown agent "${options.agent}". Use one of: ${Object.keys(agents).join(', ')}`)
78+
return
79+
}
80+
7581
print()
7682
print(` Welcome to ${colors.magenta.bold('CodeceptJS')} initialization tool`)
7783
print(' It will prepare and configure a test environment for you')
@@ -318,6 +324,27 @@ export default async function (initPath, options = {}) {
318324
print(generateDefinitionsManually)
319325
}
320326

327+
let agentName = options.agent
328+
if (!agentName && !skipPrompts) {
329+
const answer = await inquirer.prompt([
330+
{
331+
name: 'agent',
332+
type: 'list',
333+
message: 'Set up an AI coding agent?',
334+
choices: [...Object.keys(agents), 'none'],
335+
default: 'none',
336+
},
337+
])
338+
agentName = answer?.agent
339+
}
340+
341+
if (agentName && agentName !== 'none') {
342+
await setupAgent(agentName, { config: isTypeScript ? typeScriptconfigFile : configFile })
343+
} else {
344+
print()
345+
print(`Using an AI agent? Run: ${colors.green('npx codeceptjs init:agent')}`)
346+
}
347+
321348
print('')
322349
success(' Almost ready... Next step:')
323350

‎lib/command/initAgent.js‎

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import colors from 'chalk'
2+
import fs from 'fs'
3+
import path from 'path'
4+
import inquirer from 'inquirer'
5+
import { spawnSync } from 'child_process'
6+
import { mkdirp } from 'mkdirp'
7+
8+
import output from '../output.js'
9+
import { getTestRoot, findConfigFile } from './utils.js'
10+
11+
const { print, success, error } = output
12+
13+
const SERVER = 'codeceptjs'
14+
const MCP_COMMAND = ['npx', 'codeceptjs-mcp']
15+
const SKILLS_PACKAGE = 'codeceptjs/skills'
16+
17+
export const agents = {
18+
claude: {
19+
title: 'Claude Code',
20+
skills: 'claude-code',
21+
bin: 'claude',
22+
installUrl: 'https://code.claude.com/docs/en/setup',
23+
envFlag: '-e',
24+
},
25+
codex: {
26+
title: 'Codex',
27+
skills: 'codex',
28+
bin: 'codex',
29+
installUrl: 'https://developers.openai.com/codex/cli',
30+
envFlag: '--env',
31+
},
32+
cursor: {
33+
title: 'Cursor',
34+
skills: 'cursor',
35+
file: '.cursor/mcp.json',
36+
key: 'mcpServers',
37+
entry: env => ({ command: MCP_COMMAND[0], args: MCP_COMMAND.slice(1), ...(env && { env }) }),
38+
},
39+
opencode: {
40+
title: 'OpenCode',
41+
skills: 'opencode',
42+
file: 'opencode.json',
43+
key: 'mcp',
44+
entry: env => ({ type: 'local', command: MCP_COMMAND, enabled: true, ...(env && { environment: env }) }),
45+
},
46+
}
47+
48+
const agentNames = Object.keys(agents)
49+
50+
export default async function (agentName, options = {}) {
51+
if (!agentName && options.yes) return fail(`Agent is required with --yes. Use one of: ${agentNames.join(', ')}`)
52+
if (agentName && !agents[agentName]) return fail(`Unknown agent "${agentName}". Use one of: ${agentNames.join(', ')}`)
53+
54+
if (!agentName) {
55+
const answer = await inquirer.prompt([
56+
{
57+
name: 'agent',
58+
type: 'list',
59+
message: 'Which coding agent do you use?',
60+
choices: agentNames,
61+
},
62+
])
63+
agentName = answer.agent
64+
}
65+
return setupAgent(agentName, options)
66+
}
67+
68+
export async function setupAgent(agentName, options = {}) {
69+
const agent = agents[agentName]
70+
if (!agent) throw new Error(`Unknown agent "${agentName}". Use one of: ${agentNames.join(', ')}`)
71+
72+
const run = options.runner || spawnCommand
73+
const cwd = process.cwd()
74+
const env = configEnv(options.config, cwd)
75+
76+
print()
77+
print(`Setting up CodeceptJS for ${colors.bold(agent.title)}`)
78+
print()
79+
80+
let ready = agent.bin ? registerWithCli(agent, env, run) : writeMcpFile(path.join(cwd, agent.file), agent, env)
81+
82+
const skillsArgs = ['skills', 'add', SKILLS_PACKAGE, '-a', agent.skills, '-y']
83+
print(`Installing CodeceptJS skills: ${colors.green(['npx', ...skillsArgs].join(' '))}`)
84+
if (!succeeded(run('npx', skillsArgs))) {
85+
ready = false
86+
error('Installing skills failed. Run it manually:')
87+
print(` npx ${skillsArgs.join(' ')}`)
88+
}
89+
90+
print()
91+
if (ready) {
92+
success(`Start ${agent.title} in this project. CodeceptJS skills and MCP are ready.`)
93+
} else {
94+
print(`Run the commands above, then start ${agent.title} in this project.`)
95+
}
96+
print()
97+
return ready
98+
}
99+
100+
function registerWithCli(agent, env, run) {
101+
const addArgs = ['mcp', 'add', SERVER]
102+
if (env) Object.entries(env).forEach(([key, value]) => addArgs.push(agent.envFlag, `${key}=${value}`))
103+
addArgs.push('--', ...MCP_COMMAND)
104+
const manual = ` ${agent.bin} ${addArgs.join(' ')}`
105+
106+
if (!succeeded(run(agent.bin, ['--version'], { quiet: true }))) {
107+
error(`${agent.title} CLI (${agent.bin}) was not found. Install it: ${agent.installUrl}`)
108+
print('Then register the MCP server:')
109+
print(manual)
110+
return false
111+
}
112+
113+
if (succeeded(run(agent.bin, ['mcp', 'get', SERVER], { quiet: true }))) {
114+
print(`MCP server "${SERVER}" is already registered in ${agent.title}`)
115+
return true
116+
}
117+
118+
print(`Registering MCP server: ${colors.green(`${agent.bin} ${addArgs.join(' ')}`)}`)
119+
if (succeeded(run(agent.bin, addArgs))) return true
120+
121+
error('Registering MCP server failed. Run it manually:')
122+
print(manual)
123+
return false
124+
}
125+
126+
function writeMcpFile(file, agent, env) {
127+
const entry = agent.entry(env)
128+
const relative = path.relative(process.cwd(), file)
129+
let data = {}
130+
131+
if (fs.existsSync(file)) {
132+
try {
133+
data = JSON.parse(fs.readFileSync(file, 'utf8'))
134+
} catch (err) {
135+
data = null
136+
}
137+
if (!isObject(data) || (data[agent.key] !== undefined && !isObject(data[agent.key]))) {
138+
error(`Could not parse ${relative}, leaving it untouched. Add this to "${agent.key}" manually:`)
139+
print(JSON.stringify({ [SERVER]: entry }, null, 2))
140+
return false
141+
}
142+
}
143+
144+
if (data[agent.key] && JSON.stringify(data[agent.key][SERVER]) === JSON.stringify(entry)) {
145+
print(`MCP server "${SERVER}" is already configured in ${relative}`)
146+
return true
147+
}
148+
149+
data[agent.key] = { ...(data[agent.key] || {}), [SERVER]: entry }
150+
mkdirp.sync(path.dirname(file))
151+
fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`)
152+
print(`MCP server "${SERVER}" added to ${relative}`)
153+
return true
154+
}
155+
156+
function configEnv(config, cwd) {
157+
let configFile = config ? path.resolve(cwd, config) : findConfigFile(getTestRoot())
158+
if (configFile && fs.existsSync(configFile) && fs.lstatSync(configFile).isDirectory()) configFile = findConfigFile(configFile)
159+
if (!configFile || configFile === path.join(cwd, 'codecept.conf.js')) return null
160+
const relative = path.relative(cwd, configFile)
161+
return { CODECEPTJS_CONFIG: relative.startsWith('..') ? configFile : `./${relative.split(path.sep).join('/')}` }
162+
}
163+
164+
function fail(message) {
165+
error(message)
166+
process.exitCode = 1
167+
return false
168+
}
169+
170+
function isObject(value) {
171+
return !!value && typeof value === 'object' && !Array.isArray(value)
172+
}
173+
174+
function succeeded(result) {
175+
return !!result && !result.error && result.status === 0
176+
}
177+
178+
function spawnCommand(command, args, { quiet } = {}) {
179+
return spawnSync(command, args, { stdio: quiet ? 'ignore' : 'inherit', shell: process.platform === 'win32' })
180+
}

0 commit comments

Comments
 (0)