claude-coder-mac-mcp 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 pm990320
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # claude-coder-mac-mcp
2
+
3
+ An MCP server that allows Claude Desktop to spawn new Claude Code instances in iTerm2 windows.
4
+
5
+ ## Features
6
+
7
+ - **spawn_claude_coder**: Spawn a new Claude Code instance with a given prompt
8
+ - Opens a new iTerm2 window
9
+ - Optionally runs with `--dangerously-skip-permissions` (must be enabled on server)
10
+ - Passes your prompt directly to Claude
11
+ - The terminal becomes fully interactive
12
+
13
+ - **list_iterm_windows**: List all iTerm2 windows and sessions
14
+
15
+ ## Requirements
16
+
17
+ - macOS
18
+ - [iTerm2](https://iterm2.com/) installed
19
+ - [Claude Code CLI](https://claude.ai/code) installed
20
+ - Automation permissions granted to the MCP host app
21
+
22
+ ## Installation
23
+
24
+ ### Via npm (recommended)
25
+
26
+ ```bash
27
+ npm install -g claude-coder-mac-mcp
28
+ ```
29
+
30
+ ### From source
31
+
32
+ ```bash
33
+ git clone https://github.com/pm990320/claude-coder-mac-mcp.git
34
+ cd claude-coder-mac-mcp
35
+ npm install
36
+ npm run build
37
+ ```
38
+
39
+ ### Verify installation
40
+
41
+ ```bash
42
+ claude-coder-mac-mcp check
43
+ ```
44
+
45
+ This runs environment checks to verify macOS, iTerm2, Claude Code CLI, and automation permissions are all configured correctly.
46
+
47
+ ## CLI Usage
48
+
49
+ The CLI has multiple commands:
50
+
51
+ ```bash
52
+ # Show help
53
+ claude-coder-mac-mcp --help
54
+
55
+ # Start the MCP server (used by Claude Desktop)
56
+ # By default, spawned Claude instances run in normal mode (user must approve actions)
57
+ claude-coder-mac-mcp mcp
58
+
59
+ # Start the MCP server with --dangerously-skip-permissions enabled
60
+ # WARNING: This allows spawned Claude instances to run without user approval
61
+ claude-coder-mac-mcp mcp --dangerously-skip-permissions
62
+
63
+ # Test spawning a Claude Code instance directly (normal mode)
64
+ claude-coder-mac-mcp spawn "Help me write a REST API"
65
+ claude-coder-mac-mcp spawn "Fix the tests" -d ~/myproject -t "Test Fixer"
66
+
67
+ # Test spawning with --dangerously-skip-permissions
68
+ claude-coder-mac-mcp spawn "Fix the tests" --dangerously-skip-permissions
69
+
70
+ # List current iTerm2 windows
71
+ claude-coder-mac-mcp list
72
+
73
+ # Check environment is configured correctly
74
+ claude-coder-mac-mcp check
75
+ ```
76
+
77
+ ## Claude Desktop Configuration
78
+
79
+ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
80
+
81
+ **Normal mode** (user must approve Claude Code actions):
82
+ ```json
83
+ {
84
+ "mcpServers": {
85
+ "claude-coder-mac": {
86
+ "command": "node",
87
+ "args": ["/path/to/claude-coder-mac-mcp/dist/index.js", "mcp"]
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ **With --dangerously-skip-permissions** (Claude Code runs autonomously):
94
+ ```json
95
+ {
96
+ "mcpServers": {
97
+ "claude-coder-mac": {
98
+ "command": "node",
99
+ "args": ["/path/to/claude-coder-mac-mcp/dist/index.js", "mcp", "--dangerously-skip-permissions"]
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ ## Usage with Claude Desktop
106
+
107
+ Once configured, you can ask Claude Desktop to spawn new Claude Code instances:
108
+
109
+ > "Start a new Claude coder to work on my project at ~/myproject with the task: implement user authentication"
110
+
111
+ Claude will use the `spawn_claude_coder` tool to open iTerm2 with a new Claude Code session.
112
+
113
+ ## Permissions
114
+
115
+ On first use, macOS will ask you to grant automation permissions. Go to:
116
+
117
+ **System Settings > Privacy & Security > Automation**
118
+
119
+ And ensure the app running the MCP server (e.g., Claude Desktop) has permission to control iTerm2.
120
+
121
+ ## MCP Tool Parameters
122
+
123
+ ### spawn_claude_coder
124
+
125
+ | Parameter | Required | Description |
126
+ |-----------|----------|-------------|
127
+ | `prompt` | Yes | The task/prompt to give to Claude Code |
128
+ | `workingDirectory` | No | Directory to run Claude Code in (defaults to ~) |
129
+ | `windowTitle` | No | Custom title for the iTerm2 window |
130
+
131
+ Note: Whether `--dangerously-skip-permissions` is passed to spawned Claude instances is controlled by the server startup flag, not by tool parameters.
132
+
133
+ ### list_iterm_windows
134
+
135
+ No parameters. Lists all open iTerm2 windows and their sessions.
136
+
137
+ ## Development
138
+
139
+ ```bash
140
+ # Install dependencies
141
+ npm install
142
+
143
+ # Build
144
+ npm run build
145
+
146
+ # Run tests
147
+ npm test
148
+
149
+ # Lint
150
+ npm run lint
151
+
152
+ # Fix lint issues
153
+ npm run fix
154
+
155
+ # Type check
156
+ npm run typecheck
157
+ ```
158
+
159
+ ## Project Structure
160
+
161
+ ```
162
+ src/
163
+ ├── index.ts # CLI entrypoint (uses commander)
164
+ ├── server.ts # MCP server setup and tool registration
165
+ ├── spawn.ts # Core spawn functionality
166
+ ├── iterm.ts # iTerm2 AppleScript utilities
167
+ └── check.ts # Environment verification checks
168
+
169
+ tests/
170
+ ├── spawn.test.ts # Unit tests for spawn functions
171
+ └── iterm.test.ts # Unit tests for iTerm utilities
172
+ ```
173
+
174
+ The modules are separated for testability - `spawn.ts` and `iterm.ts` contain pure functions that can be unit tested independently.
175
+
176
+ ## License
177
+
178
+ MIT
@@ -0,0 +1,37 @@
1
+ export interface CheckResult {
2
+ name: string;
3
+ status: 'pass' | 'fail' | 'warn';
4
+ message: string;
5
+ }
6
+ export interface EnvironmentCheckResult {
7
+ allPassed: boolean;
8
+ checks: CheckResult[];
9
+ }
10
+ /**
11
+ * Check if running on macOS
12
+ */
13
+ export declare function checkMacOS(): CheckResult;
14
+ /**
15
+ * Check if iTerm2 is installed
16
+ */
17
+ export declare function checkITerm2(): Promise<CheckResult>;
18
+ /**
19
+ * Check if Claude Code CLI is installed
20
+ */
21
+ export declare function checkClaudeCode(): Promise<CheckResult>;
22
+ /**
23
+ * Check if automation permissions are granted for iTerm2
24
+ */
25
+ export declare function checkAutomationPermissions(): Promise<CheckResult>;
26
+ /**
27
+ * Check if Node.js version meets requirements
28
+ */
29
+ export declare function checkNodeVersion(): CheckResult;
30
+ /**
31
+ * Run all environment checks
32
+ */
33
+ export declare function runAllChecks(): Promise<EnvironmentCheckResult>;
34
+ /**
35
+ * Format check results for display
36
+ */
37
+ export declare function formatCheckResults(result: EnvironmentCheckResult): string;
package/dist/check.js ADDED
@@ -0,0 +1,173 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import * as os from 'os';
4
+ const execAsync = promisify(exec);
5
+ /**
6
+ * Check if running on macOS
7
+ */
8
+ export function checkMacOS() {
9
+ const platform = os.platform();
10
+ if (platform === 'darwin') {
11
+ const release = os.release();
12
+ return {
13
+ name: 'macOS',
14
+ status: 'pass',
15
+ message: `Running on macOS (Darwin ${release})`,
16
+ };
17
+ }
18
+ return {
19
+ name: 'macOS',
20
+ status: 'fail',
21
+ message: `This tool requires macOS. Current platform: ${platform}`,
22
+ };
23
+ }
24
+ /**
25
+ * Check if iTerm2 is installed
26
+ */
27
+ export async function checkITerm2() {
28
+ try {
29
+ // Check if iTerm2 app exists
30
+ await execAsync('test -d "/Applications/iTerm.app"');
31
+ return {
32
+ name: 'iTerm2',
33
+ status: 'pass',
34
+ message: 'iTerm2 is installed at /Applications/iTerm.app',
35
+ };
36
+ }
37
+ catch {
38
+ return {
39
+ name: 'iTerm2',
40
+ status: 'fail',
41
+ message: 'iTerm2 is not installed. Download from https://iterm2.com/ or install via: brew install --cask iterm2',
42
+ };
43
+ }
44
+ }
45
+ /**
46
+ * Check if Claude Code CLI is installed
47
+ */
48
+ export async function checkClaudeCode() {
49
+ try {
50
+ const { stdout } = await execAsync('which claude');
51
+ const path = stdout.trim();
52
+ // Try to get version
53
+ try {
54
+ const { stdout: versionOut } = await execAsync('claude --version');
55
+ const version = versionOut.trim();
56
+ return {
57
+ name: 'Claude Code CLI',
58
+ status: 'pass',
59
+ message: `Claude Code CLI found at ${path} (${version})`,
60
+ };
61
+ }
62
+ catch {
63
+ return {
64
+ name: 'Claude Code CLI',
65
+ status: 'pass',
66
+ message: `Claude Code CLI found at ${path}`,
67
+ };
68
+ }
69
+ }
70
+ catch {
71
+ return {
72
+ name: 'Claude Code CLI',
73
+ status: 'fail',
74
+ message: 'Claude Code CLI is not installed. Install from https://claude.ai/code',
75
+ };
76
+ }
77
+ }
78
+ /**
79
+ * Check if automation permissions are granted for iTerm2
80
+ */
81
+ export async function checkAutomationPermissions() {
82
+ try {
83
+ // Try a simple AppleScript that requires iTerm2 automation permission
84
+ await execAsync('osascript -e \'tell application "iTerm2" to get name\'');
85
+ return {
86
+ name: 'Automation Permissions',
87
+ status: 'pass',
88
+ message: 'Automation permissions for iTerm2 are granted',
89
+ };
90
+ }
91
+ catch (error) {
92
+ const errorMessage = error instanceof Error ? error.message : String(error);
93
+ if (errorMessage.includes('not allowed') || errorMessage.includes('1743')) {
94
+ return {
95
+ name: 'Automation Permissions',
96
+ status: 'fail',
97
+ message: 'Automation permissions not granted. Go to System Settings > Privacy & Security > Automation and enable iTerm2 for your terminal/app.',
98
+ };
99
+ }
100
+ // iTerm2 might not be running, which is OK
101
+ if (errorMessage.includes('not running') ||
102
+ errorMessage.includes('(-600)')) {
103
+ return {
104
+ name: 'Automation Permissions',
105
+ status: 'warn',
106
+ message: 'Could not verify automation permissions (iTerm2 is not running). Permissions will be requested on first use.',
107
+ };
108
+ }
109
+ return {
110
+ name: 'Automation Permissions',
111
+ status: 'warn',
112
+ message: `Could not verify automation permissions: ${errorMessage}`,
113
+ };
114
+ }
115
+ }
116
+ /**
117
+ * Check if Node.js version meets requirements
118
+ */
119
+ export function checkNodeVersion() {
120
+ const nodeVersion = process.version;
121
+ const major = parseInt(nodeVersion.slice(1).split('.')[0], 10);
122
+ if (major >= 18) {
123
+ return {
124
+ name: 'Node.js',
125
+ status: 'pass',
126
+ message: `Node.js ${nodeVersion} meets minimum requirement (>=18.0.0)`,
127
+ };
128
+ }
129
+ return {
130
+ name: 'Node.js',
131
+ status: 'fail',
132
+ message: `Node.js ${nodeVersion} is below minimum requirement (>=18.0.0)`,
133
+ };
134
+ }
135
+ /**
136
+ * Run all environment checks
137
+ */
138
+ export async function runAllChecks() {
139
+ const checks = [];
140
+ // Synchronous checks
141
+ checks.push(checkMacOS());
142
+ checks.push(checkNodeVersion());
143
+ // Async checks
144
+ checks.push(await checkITerm2());
145
+ checks.push(await checkClaudeCode());
146
+ checks.push(await checkAutomationPermissions());
147
+ const allPassed = checks.every(c => c.status === 'pass' || c.status === 'warn');
148
+ return { allPassed, checks };
149
+ }
150
+ /**
151
+ * Format check results for display
152
+ */
153
+ export function formatCheckResults(result) {
154
+ const lines = ['Environment Check Results', '='.repeat(50), ''];
155
+ for (const check of result.checks) {
156
+ const icon = check.status === 'pass'
157
+ ? '[OK]'
158
+ : check.status === 'warn'
159
+ ? '[!!]'
160
+ : '[X]';
161
+ lines.push(`${icon} ${check.name}`);
162
+ lines.push(` ${check.message}`);
163
+ lines.push('');
164
+ }
165
+ lines.push('='.repeat(50));
166
+ if (result.allPassed) {
167
+ lines.push('All checks passed! Environment is ready.');
168
+ }
169
+ else {
170
+ lines.push('Some checks failed. Please fix the issues above.');
171
+ }
172
+ return lines.join('\n');
173
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { startServer } from './server.js';
4
+ import { spawnClaudeCoder } from './spawn.js';
5
+ import { listItermWindows, formatWindowList } from './iterm.js';
6
+ import { runAllChecks, formatCheckResults } from './check.js';
7
+ const program = new Command();
8
+ program
9
+ .name('claude-coder-mac-mcp')
10
+ .description('Spawn Claude Code instances from Claude Desktop via MCP')
11
+ .version('1.0.0');
12
+ program
13
+ .command('mcp')
14
+ .alias('serve')
15
+ .description('Start the MCP server (for Claude Desktop)')
16
+ .option('--dangerously-skip-permissions', 'Pass --dangerously-skip-permissions to spawned Claude instances')
17
+ .action(async (options) => {
18
+ await startServer({
19
+ dangerouslySkipPermissions: options.dangerouslySkipPermissions ?? false,
20
+ });
21
+ });
22
+ program
23
+ .command('spawn')
24
+ .description('Test spawning a Claude Code instance in iTerm2')
25
+ .argument('<prompt>', 'The prompt/task to give to Claude Code')
26
+ .option('-d, --directory <path>', 'Working directory', process.env.HOME)
27
+ .option('-t, --title <title>', 'Window title', 'Claude Coder')
28
+ .option('--dangerously-skip-permissions', 'Pass --dangerously-skip-permissions to the Claude instance')
29
+ .action(async (prompt, options) => {
30
+ const skipPerms = options.dangerouslySkipPermissions ?? false;
31
+ const modeLabel = skipPerms
32
+ ? 'with --dangerously-skip-permissions'
33
+ : 'in normal mode';
34
+ console.log(`Spawning Claude Code instance (${modeLabel})...`);
35
+ const result = await spawnClaudeCoder({
36
+ prompt,
37
+ workingDirectory: options.directory,
38
+ windowTitle: options.title,
39
+ dangerouslySkipPermissions: skipPerms,
40
+ });
41
+ if (result.success) {
42
+ console.log('Success!');
43
+ console.log(` Window title: ${result.windowTitle}`);
44
+ console.log(` Working directory: ${result.workingDirectory}`);
45
+ console.log(` Mode: ${modeLabel}`);
46
+ console.log(` Prompt: ${result.prompt}`);
47
+ }
48
+ else {
49
+ console.error(`Failed: ${result.error}`);
50
+ process.exit(1);
51
+ }
52
+ });
53
+ program
54
+ .command('list')
55
+ .alias('ls')
56
+ .description('List iTerm2 windows and sessions')
57
+ .action(async () => {
58
+ const windows = await listItermWindows();
59
+ console.log(formatWindowList(windows));
60
+ });
61
+ program
62
+ .command('check')
63
+ .alias('doctor')
64
+ .description('Check if the environment is configured correctly')
65
+ .action(async () => {
66
+ const result = await runAllChecks();
67
+ console.log(formatCheckResults(result));
68
+ if (!result.allPassed) {
69
+ process.exit(1);
70
+ }
71
+ });
72
+ program.parse();
@@ -0,0 +1,24 @@
1
+ export interface ItermWindow {
2
+ id: string;
3
+ sessions: ItermSession[];
4
+ }
5
+ export interface ItermSession {
6
+ name: string;
7
+ tty: string;
8
+ }
9
+ /**
10
+ * Execute an AppleScript and return the result
11
+ */
12
+ export declare function execAppleScript(script: string): Promise<string>;
13
+ /**
14
+ * List all iTerm2 windows and their sessions
15
+ */
16
+ export declare function listItermWindows(): Promise<ItermWindow[]>;
17
+ /**
18
+ * Parse the window list output from AppleScript
19
+ */
20
+ export declare function parseWindowList(output: string): ItermWindow[];
21
+ /**
22
+ * Format window list for display
23
+ */
24
+ export declare function formatWindowList(windows: ItermWindow[]): string;
package/dist/iterm.js ADDED
@@ -0,0 +1,84 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ const execAsync = promisify(exec);
4
+ /**
5
+ * Execute an AppleScript and return the result
6
+ */
7
+ export async function execAppleScript(script) {
8
+ // Escape single quotes for shell
9
+ const escapedScript = script.replace(/'/g, "'\"'\"'");
10
+ const { stdout } = await execAsync(`osascript -e '${escapedScript}'`);
11
+ return stdout;
12
+ }
13
+ /**
14
+ * List all iTerm2 windows and their sessions
15
+ */
16
+ export async function listItermWindows() {
17
+ const appleScript = `
18
+ tell application "iTerm"
19
+ set output to ""
20
+ repeat with w in windows
21
+ set output to output & "WINDOW:" & (id of w) & linefeed
22
+ repeat with t in tabs of w
23
+ repeat with s in sessions of t
24
+ set output to output & "SESSION:" & (name of s) & "|" & (tty of s) & linefeed
25
+ end repeat
26
+ end repeat
27
+ end repeat
28
+ return output
29
+ end tell
30
+ `;
31
+ try {
32
+ const stdout = await execAppleScript(appleScript);
33
+ return parseWindowList(stdout);
34
+ }
35
+ catch {
36
+ return [];
37
+ }
38
+ }
39
+ /**
40
+ * Parse the window list output from AppleScript
41
+ */
42
+ export function parseWindowList(output) {
43
+ const windows = [];
44
+ let currentWindow = null;
45
+ const lines = output.split('\n').filter(line => line.trim());
46
+ for (const line of lines) {
47
+ if (line.startsWith('WINDOW:')) {
48
+ if (currentWindow) {
49
+ windows.push(currentWindow);
50
+ }
51
+ currentWindow = {
52
+ id: line.substring(7),
53
+ sessions: [],
54
+ };
55
+ }
56
+ else if (line.startsWith('SESSION:') && currentWindow) {
57
+ const parts = line.substring(8).split('|');
58
+ currentWindow.sessions.push({
59
+ name: parts[0] || '',
60
+ tty: parts[1] || '',
61
+ });
62
+ }
63
+ }
64
+ if (currentWindow) {
65
+ windows.push(currentWindow);
66
+ }
67
+ return windows;
68
+ }
69
+ /**
70
+ * Format window list for display
71
+ */
72
+ export function formatWindowList(windows) {
73
+ if (windows.length === 0) {
74
+ return 'No iTerm2 windows found.';
75
+ }
76
+ const lines = [];
77
+ for (const window of windows) {
78
+ lines.push(`Window: ${window.id}`);
79
+ for (const session of window.sessions) {
80
+ lines.push(` Session: ${session.name} - ${session.tty}`);
81
+ }
82
+ }
83
+ return lines.join('\n');
84
+ }
@@ -0,0 +1,16 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export interface ServerOptions {
3
+ dangerouslySkipPermissions: boolean;
4
+ }
5
+ /**
6
+ * Create and configure the MCP server
7
+ */
8
+ export declare function createServer(options: ServerOptions): McpServer;
9
+ /**
10
+ * Register all MCP tools on the server
11
+ */
12
+ export declare function registerTools(server: McpServer, options: ServerOptions): void;
13
+ /**
14
+ * Start the MCP server with stdio transport
15
+ */
16
+ export declare function startServer(options: ServerOptions): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,142 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { spawnClaudeCoder } from './spawn.js';
5
+ import { listItermWindows, formatWindowList } from './iterm.js';
6
+ /**
7
+ * Create and configure the MCP server
8
+ */
9
+ export function createServer(options) {
10
+ const server = new McpServer({
11
+ name: 'claude-coder-mac-mcp',
12
+ version: '1.0.0',
13
+ });
14
+ registerTools(server, options);
15
+ return server;
16
+ }
17
+ /**
18
+ * Register all MCP tools on the server
19
+ */
20
+ export function registerTools(server, options) {
21
+ const { dangerouslySkipPermissions } = options;
22
+ const permissionMode = dangerouslySkipPermissions
23
+ ? 'AUTONOMOUS MODE (--dangerously-skip-permissions): Claude will act without user approval. Be specific to prevent unintended actions.'
24
+ : 'INTERACTIVE MODE: User must approve each action in the spawned terminal.';
25
+ const toolDescription = `Spawn a new Claude Code instance in an iTerm2 window to work on a task.
26
+
27
+ ${permissionMode}
28
+
29
+ PROMPT BEST PRACTICES:
30
+ - Be specific: "Run npm test, fix any failing tests" not "fix tests"
31
+ - Include context Claude can't see: "This is a Next.js app using Prisma and PostgreSQL"
32
+ - Set success criteria: "Done when all tests pass and npm run build succeeds"
33
+ - Mention key files: "The API routes are in src/app/api/"
34
+ - One focused task works better than multiple vague ones
35
+
36
+ EXAMPLE PROMPT:
37
+ "In this TypeScript Express project, add a new GET /api/health endpoint that returns {status: 'ok', timestamp: Date.now()}. Follow the pattern in src/routes/users.ts. Run npm test when done."
38
+
39
+ ALWAYS specify workingDirectory so Claude starts in the correct project folder. If you do not know the correct working directory, specify as the first instruction in your prompt that claude should find the project and cd into its directory first.`;
40
+ server.registerTool('spawn_claude_coder', {
41
+ title: 'Spawn Claude Coder',
42
+ description: toolDescription,
43
+ inputSchema: {
44
+ prompt: z
45
+ .string()
46
+ .describe('The prompt/task to give to the new Claude Code instance'),
47
+ workingDirectory: z
48
+ .string()
49
+ .optional()
50
+ .describe('The working directory for the Claude Code instance (defaults to home directory)'),
51
+ windowTitle: z
52
+ .string()
53
+ .optional()
54
+ .describe("Custom title for the iTerm2 window (defaults to 'Claude Coder')"),
55
+ },
56
+ annotations: {
57
+ title: 'Spawn Claude Coder',
58
+ readOnlyHint: false,
59
+ destructiveHint: dangerouslySkipPermissions,
60
+ idempotentHint: false,
61
+ openWorldHint: true,
62
+ },
63
+ }, async ({ prompt, workingDirectory, windowTitle }) => {
64
+ const result = await spawnClaudeCoder({
65
+ prompt,
66
+ workingDirectory,
67
+ windowTitle,
68
+ dangerouslySkipPermissions,
69
+ });
70
+ if (result.success) {
71
+ const permissionStatus = result.dangerouslySkipPermissions
72
+ ? 'with --dangerously-skip-permissions'
73
+ : 'in normal mode';
74
+ return {
75
+ content: [
76
+ {
77
+ type: 'text',
78
+ text: `Successfully spawned Claude Code in iTerm2 (${permissionStatus})!\n\nWindow title: ${result.windowTitle}\nWorking directory: ${result.workingDirectory}\nPrompt: ${result.prompt}\n\nThe Claude Code instance is now running interactively in iTerm2.`,
79
+ },
80
+ ],
81
+ };
82
+ }
83
+ else {
84
+ return {
85
+ content: [
86
+ {
87
+ type: 'text',
88
+ text: `Failed to spawn Claude Code: ${result.error}\n\nMake sure iTerm2 is installed and you have granted automation permissions in System Settings > Privacy & Security > Automation.`,
89
+ },
90
+ ],
91
+ isError: true,
92
+ };
93
+ }
94
+ });
95
+ server.registerTool('list_iterm_windows', {
96
+ title: 'List iTerm2 Windows',
97
+ description: 'List all iTerm2 windows and their sessions (useful to see running Claude instances)',
98
+ annotations: {
99
+ title: 'List iTerm2 Windows',
100
+ readOnlyHint: true,
101
+ destructiveHint: false,
102
+ idempotentHint: true,
103
+ openWorldHint: true,
104
+ },
105
+ }, async () => {
106
+ try {
107
+ const windows = await listItermWindows();
108
+ return {
109
+ content: [
110
+ {
111
+ type: 'text',
112
+ text: formatWindowList(windows),
113
+ },
114
+ ],
115
+ };
116
+ }
117
+ catch (error) {
118
+ const errorMessage = error instanceof Error ? error.message : String(error);
119
+ return {
120
+ content: [
121
+ {
122
+ type: 'text',
123
+ text: `Failed to list iTerm windows: ${errorMessage}`,
124
+ },
125
+ ],
126
+ isError: true,
127
+ };
128
+ }
129
+ });
130
+ }
131
+ /**
132
+ * Start the MCP server with stdio transport
133
+ */
134
+ export async function startServer(options) {
135
+ const server = createServer(options);
136
+ const transport = new StdioServerTransport();
137
+ await server.connect(transport);
138
+ const modeLabel = options.dangerouslySkipPermissions
139
+ ? 'with --dangerously-skip-permissions'
140
+ : 'in normal mode';
141
+ console.error(`Claude Coder Mac MCP server running on stdio (${modeLabel})`);
142
+ }
@@ -0,0 +1,38 @@
1
+ export interface SpawnOptions {
2
+ prompt: string;
3
+ workingDirectory?: string;
4
+ windowTitle?: string;
5
+ dangerouslySkipPermissions?: boolean;
6
+ }
7
+ export interface SpawnResult {
8
+ success: boolean;
9
+ windowTitle: string;
10
+ workingDirectory: string;
11
+ prompt: string;
12
+ dangerouslySkipPermissions: boolean;
13
+ error?: string;
14
+ }
15
+ /**
16
+ * Escape a string for use in AppleScript
17
+ */
18
+ export declare function escapeForAppleScript(str: string): string;
19
+ /**
20
+ * Escape a string for use in shell (inside the AppleScript command)
21
+ */
22
+ export declare function escapeForShell(str: string): string;
23
+ /**
24
+ * Build the claude command string
25
+ */
26
+ export declare function buildClaudeCommand(prompt: string, dangerouslySkipPermissions: boolean): string;
27
+ /**
28
+ * Build the full shell command (cd + claude)
29
+ */
30
+ export declare function buildFullCommand(prompt: string, workingDirectory: string, dangerouslySkipPermissions: boolean): string;
31
+ /**
32
+ * Build the AppleScript to spawn an iTerm2 window with a command
33
+ */
34
+ export declare function buildSpawnAppleScript(command: string, windowTitle: string): string;
35
+ /**
36
+ * Spawn a new Claude Code instance in iTerm2
37
+ */
38
+ export declare function spawnClaudeCoder(options: SpawnOptions): Promise<SpawnResult>;
package/dist/spawn.js ADDED
@@ -0,0 +1,76 @@
1
+ import { execAppleScript } from './iterm.js';
2
+ /**
3
+ * Escape a string for use in AppleScript
4
+ */
5
+ export function escapeForAppleScript(str) {
6
+ return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
7
+ }
8
+ /**
9
+ * Escape a string for use in shell (inside the AppleScript command)
10
+ */
11
+ export function escapeForShell(str) {
12
+ // Use single quotes and escape any single quotes in the string
13
+ return `'${str.replace(/'/g, "'\"'\"'")}'`;
14
+ }
15
+ /**
16
+ * Build the claude command string
17
+ */
18
+ export function buildClaudeCommand(prompt, dangerouslySkipPermissions) {
19
+ const flags = dangerouslySkipPermissions
20
+ ? '--dangerously-skip-permissions '
21
+ : '';
22
+ return `claude ${flags}${escapeForShell(prompt)}`;
23
+ }
24
+ /**
25
+ * Build the full shell command (cd + claude)
26
+ */
27
+ export function buildFullCommand(prompt, workingDirectory, dangerouslySkipPermissions) {
28
+ const claudeCommand = buildClaudeCommand(prompt, dangerouslySkipPermissions);
29
+ return `cd ${escapeForShell(workingDirectory)} && ${claudeCommand}`;
30
+ }
31
+ /**
32
+ * Build the AppleScript to spawn an iTerm2 window with a command
33
+ */
34
+ export function buildSpawnAppleScript(command, windowTitle) {
35
+ return `
36
+ tell application "iTerm"
37
+ activate
38
+ set newWindow to (create window with default profile)
39
+ tell current session of newWindow
40
+ set name to "${escapeForAppleScript(windowTitle)}"
41
+ write text "${escapeForAppleScript(command)}"
42
+ end tell
43
+ end tell
44
+ `;
45
+ }
46
+ /**
47
+ * Spawn a new Claude Code instance in iTerm2
48
+ */
49
+ export async function spawnClaudeCoder(options) {
50
+ const windowTitle = options.windowTitle || 'Claude Coder';
51
+ const workingDirectory = options.workingDirectory || process.env.HOME || '~';
52
+ const dangerouslySkipPermissions = options.dangerouslySkipPermissions ?? false;
53
+ const fullCommand = buildFullCommand(options.prompt, workingDirectory, dangerouslySkipPermissions);
54
+ const appleScript = buildSpawnAppleScript(fullCommand, windowTitle);
55
+ try {
56
+ await execAppleScript(appleScript);
57
+ return {
58
+ success: true,
59
+ windowTitle,
60
+ workingDirectory,
61
+ prompt: options.prompt,
62
+ dangerouslySkipPermissions,
63
+ };
64
+ }
65
+ catch (error) {
66
+ const errorMessage = error instanceof Error ? error.message : String(error);
67
+ return {
68
+ success: false,
69
+ windowTitle,
70
+ workingDirectory,
71
+ prompt: options.prompt,
72
+ dangerouslySkipPermissions,
73
+ error: errorMessage,
74
+ };
75
+ }
76
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "claude-coder-mac-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server to spawn Claude Code instances in iTerm2",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "claude-coder-mac-mcp": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.build.json",
18
+ "dev": "tsc -p tsconfig.build.json --watch",
19
+ "start": "node dist/index.js",
20
+ "lint": "gts lint",
21
+ "fix": "gts fix",
22
+ "clean": "gts clean",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "test:coverage": "vitest run --coverage",
27
+ "prepare": "npm run build",
28
+ "pretest": "npm run build",
29
+ "posttest": "npm run lint",
30
+ "prepublishOnly": "npm run build && npm test"
31
+ },
32
+ "keywords": [
33
+ "mcp",
34
+ "model-context-protocol",
35
+ "claude",
36
+ "claude-code",
37
+ "iterm2",
38
+ "macos",
39
+ "terminal",
40
+ "automation"
41
+ ],
42
+ "author": "pm990320",
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/pm990320/claude-coder-mac-mcp.git"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/pm990320/claude-coder-mac-mcp/issues"
50
+ },
51
+ "homepage": "https://github.com/pm990320/claude-coder-mac-mcp#readme",
52
+ "engines": {
53
+ "node": ">=18.0.0"
54
+ },
55
+ "dependencies": {
56
+ "@modelcontextprotocol/sdk": "^1.25.3",
57
+ "commander": "^14.0.3",
58
+ "zod": "^4.3.6"
59
+ },
60
+ "devDependencies": {
61
+ "@types/node": "^25.2.0",
62
+ "eslint-plugin-security": "^3.0.1",
63
+ "gts": "^7.0.0",
64
+ "typescript": "^5.9.3",
65
+ "vitest": "^4.0.18"
66
+ }
67
+ }