Skip to content

Commit 384aba8

Browse files
committed
feat: add mcp install command
1 parent 9b9b276 commit 384aba8

File tree

7 files changed

+875
-3
lines changed

7 files changed

+875
-3
lines changed

README.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<p align="center">
1212
<a href="https://www.npmjs.com/package/%40brightdata%2Fcli"><img src="https://img.shields.io/npm/v/%40brightdata%2Fcli?color=black&label=npm" alt="npm version" /></a>
1313
<img src="https://img.shields.io/badge/node-%3E%3D20-black" alt="node requirement" />
14-
<img src="https://img.shields.io/badge/license-ISC-black" alt="license" />
14+
<img src="https://img.shields.io/badge/license-MIT-black" alt="license" />
1515
</p>
1616

1717
---
@@ -28,6 +28,7 @@
2828
| `brightdata zones` | List and inspect your Bright Data proxy zones |
2929
| `brightdata budget` | View account balance and per-zone cost & bandwidth |
3030
| `brightdata skill` | Install Bright Data AI agent skills into your coding agent |
31+
| `brightdata add mcp` | Add the Bright Data MCP server to Claude Code, Cursor, or Codex |
3132
| `brightdata config` | Manage CLI configuration |
3233
| `brightdata init` | Interactive setup wizard |
3334

@@ -47,6 +48,7 @@
4748
- [zones](#zones)
4849
- [budget](#budget)
4950
- [skill](#skill)
51+
- [add mcp](#add-mcp)
5052
- [config](#config)
5153
- [login / logout](#login--logout)
5254
- [Configuration](#configuration)
@@ -105,6 +107,9 @@ brightdata pipelines linkedin_person_profile "https://linkedin.com/in/username"
105107

106108
# 5. Check your account balance
107109
brightdata budget
110+
111+
# 6. Install the Bright Data MCP server into your coding agent
112+
brightdata add mcp
108113
```
109114

110115
---
@@ -131,6 +136,8 @@ On first login the CLI checks for required zones (`cli_unlocker`, `cli_browser`)
131136
brightdata logout
132137
```
133138

139+
`brightdata add mcp` uses the API key stored by `brightdata login`. It does not currently read `BRIGHTDATA_API_KEY` or the global `--api-key` flag, so log in first before using it.
140+
134141
---
135142

136143
## Commands
@@ -397,6 +404,54 @@ brightdata skill list
397404

398405
---
399406

407+
### `add mcp`
408+
409+
Write a Bright Data MCP server entry into Claude Code, Cursor, or Codex config files using the API key already stored by `brightdata login`.
410+
411+
```bash
412+
brightdata add mcp # Interactive agent + scope prompts
413+
brightdata add mcp --agent claude-code --global
414+
brightdata add mcp --agent claude-code,cursor --project
415+
brightdata add mcp --agent codex --global
416+
```
417+
418+
| Flag | Description |
419+
|---|---|
420+
| `--agent <agents>` | Comma-separated targets: `claude-code,cursor,codex` |
421+
| `--global` | Install to the agent's global config file |
422+
| `--project` | Install to the current project's config file |
423+
424+
**Config targets**
425+
426+
| Agent | Global path | Project path |
427+
|---|---|---|
428+
| Claude Code | `~/.claude.json` | `.claude/settings.json` |
429+
| Cursor | `~/.cursor/mcp.json` | `.cursor/mcp.json` |
430+
| Codex | `$CODEX_HOME/mcp.json` or `~/.codex/mcp.json` | Not supported |
431+
432+
The command writes the MCP server under `mcpServers["bright-data"]`:
433+
434+
```json
435+
{
436+
"mcpServers": {
437+
"bright-data": {
438+
"command": "npx",
439+
"args": ["@brightdata/mcp"],
440+
"env": {
441+
"API_TOKEN": "<stored-api-key>"
442+
}
443+
}
444+
}
445+
}
446+
```
447+
448+
Behavior notes:
449+
- Existing config is preserved; only `mcpServers["bright-data"]` is added or replaced.
450+
- If the target config contains invalid JSON, the CLI warns and offers to overwrite it in interactive mode.
451+
- In non-interactive mode, pass both `--agent` and the appropriate scope flag to skip prompts.
452+
453+
---
454+
400455
### `config`
401456

402457
View and manage CLI configuration.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@brightdata/cli",
3-
"version": "0.1.3",
3+
"version": "0.1.4",
44
"description": "Command-line interface for Bright Data. Scrape, search, extract structured data, and automate browsers directly from your terminal.",
55
"main": "dist/index.js",
66
"bin": {
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import fs from 'fs';
2+
import os from 'os';
3+
import path from 'path';
4+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
5+
6+
const mocks = vi.hoisted(()=>({
7+
checkbox: vi.fn(),
8+
select: vi.fn(),
9+
confirm: vi.fn(),
10+
get_api_key: vi.fn(),
11+
dim: vi.fn((msg: string)=>msg),
12+
green: vi.fn((msg: string)=>msg),
13+
red: vi.fn((msg: string)=>msg),
14+
warn: vi.fn(),
15+
}));
16+
17+
vi.mock('@inquirer/prompts', ()=>({
18+
checkbox: mocks.checkbox,
19+
select: mocks.select,
20+
confirm: mocks.confirm,
21+
}));
22+
23+
vi.mock('../../utils/credentials', ()=>({
24+
get_api_key: mocks.get_api_key,
25+
}));
26+
27+
vi.mock('../../utils/output', ()=>({
28+
dim: mocks.dim,
29+
green: mocks.green,
30+
red: mocks.red,
31+
warn: mocks.warn,
32+
}));
33+
34+
import {run_add_mcp} from '../../commands/add-mcp';
35+
36+
const get_expected_entry = (api_key: string)=>({
37+
command: 'npx',
38+
args: ['@brightdata/mcp'],
39+
env: {
40+
API_TOKEN: api_key,
41+
},
42+
});
43+
44+
const mk_tmp_dir = ()=>fs.mkdtempSync(path.join(os.tmpdir(),
45+
'brightdata-add-mcp-'));
46+
47+
const read_json = (file_path: string)=>JSON.parse(fs.readFileSync(file_path,
48+
'utf8'));
49+
50+
describe('commands/add-mcp', ()=>{
51+
let tmp_dir = '';
52+
let home_dir = '';
53+
let project_dir = '';
54+
let codex_home = '';
55+
let original_cwd = '';
56+
let stdin_tty: PropertyDescriptor|undefined;
57+
let stdout_tty: PropertyDescriptor|undefined;
58+
59+
beforeEach(()=>{
60+
vi.clearAllMocks();
61+
tmp_dir = mk_tmp_dir();
62+
home_dir = path.join(tmp_dir, 'home');
63+
project_dir = path.join(tmp_dir, 'project');
64+
codex_home = path.join(tmp_dir, 'codex-home');
65+
original_cwd = process.cwd();
66+
stdin_tty = Object.getOwnPropertyDescriptor(process.stdin, 'isTTY');
67+
stdout_tty = Object.getOwnPropertyDescriptor(process.stdout, 'isTTY');
68+
69+
fs.mkdirSync(home_dir, {recursive: true});
70+
fs.mkdirSync(project_dir, {recursive: true});
71+
process.chdir(project_dir);
72+
process.env['CODEX_HOME'] = codex_home;
73+
vi.spyOn(os, 'homedir').mockReturnValue(home_dir);
74+
Object.defineProperty(process.stdin, 'isTTY', {
75+
value: true,
76+
configurable: true,
77+
});
78+
Object.defineProperty(process.stdout, 'isTTY', {
79+
value: true,
80+
configurable: true,
81+
});
82+
83+
mocks.get_api_key.mockReturnValue('test_api_key');
84+
mocks.checkbox.mockResolvedValue(['claude-code', 'cursor', 'codex']);
85+
mocks.select.mockResolvedValue('project');
86+
mocks.confirm.mockResolvedValue(true);
87+
vi.spyOn(process.stderr, 'write').mockImplementation(()=>true);
88+
});
89+
90+
afterEach(()=>{
91+
process.chdir(original_cwd);
92+
if (stdin_tty)
93+
Object.defineProperty(process.stdin, 'isTTY', stdin_tty);
94+
if (stdout_tty)
95+
Object.defineProperty(process.stdout, 'isTTY', stdout_tty);
96+
delete process.env['CODEX_HOME'];
97+
vi.restoreAllMocks();
98+
if (tmp_dir)
99+
fs.rmSync(tmp_dir, {recursive: true, force: true});
100+
});
101+
102+
it('writes selected agent configs in the full interactive flow', async()=>{
103+
await run_add_mcp();
104+
105+
expect(mocks.checkbox).toHaveBeenCalledOnce();
106+
expect(mocks.select).toHaveBeenCalledOnce();
107+
expect(mocks.confirm).not.toHaveBeenCalled();
108+
expect(read_json(path.join(project_dir, '.claude', 'settings.json')))
109+
.toEqual({
110+
mcpServers: {
111+
'bright-data': get_expected_entry('test_api_key'),
112+
},
113+
});
114+
expect(read_json(path.join(project_dir, '.cursor', 'mcp.json')))
115+
.toEqual({
116+
mcpServers: {
117+
'bright-data': get_expected_entry('test_api_key'),
118+
},
119+
});
120+
expect(read_json(path.join(codex_home, 'mcp.json')))
121+
.toEqual({
122+
mcpServers: {
123+
'bright-data': get_expected_entry('test_api_key'),
124+
},
125+
});
126+
expect(fs.existsSync(path.join(home_dir, '.claude.json'))).toBe(false);
127+
expect(fs.existsSync(path.join(home_dir, '.cursor', 'mcp.json')))
128+
.toBe(false);
129+
});
130+
131+
it('warns and overwrites invalid JSON after confirmation', async()=>{
132+
const cursor_config = path.join(project_dir, '.cursor', 'mcp.json');
133+
134+
fs.mkdirSync(path.dirname(cursor_config), {recursive: true});
135+
fs.writeFileSync(cursor_config, '{invalid-json');
136+
mocks.checkbox.mockResolvedValue(['cursor']);
137+
mocks.select.mockResolvedValue('project');
138+
mocks.confirm.mockResolvedValue(true);
139+
140+
await run_add_mcp();
141+
142+
expect(mocks.warn).toHaveBeenCalledWith(
143+
expect.stringContaining('Invalid JSON in '+cursor_config)
144+
);
145+
expect(mocks.confirm).toHaveBeenCalledWith({
146+
message: 'Overwrite invalid config at '+cursor_config+'?',
147+
default: false,
148+
});
149+
expect(read_json(cursor_config)).toEqual({
150+
mcpServers: {
151+
'bright-data': get_expected_entry('test_api_key'),
152+
},
153+
});
154+
});
155+
});

0 commit comments

Comments
 (0)