Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,61 @@ blocked_commands:
- `examples/org_config.yaml` - Org config example (all commented by default)
- `examples/README.md` - Comprehensive guide with use cases, testing, and troubleshooting

#### Custom MCP Servers

The agent supports adding custom MCP (Model Context Protocol) servers via configuration files. This allows extending the agent's capabilities with additional tools.

**Configuration Locations:**
- **Org-level**: `~/.autocoder/config.yaml` - Available to ALL projects
- **Project-level**: `{project}/.autocoder/allowed_commands.yaml` - Project-specific

**MCP Server Config Schema:**

```yaml
# ~/.autocoder/config.yaml (org-level)
version: 1

mcp_servers:
- name: filesystem
command: npx
args:
- "@anthropic/mcp-server-filesystem"
- "${PROJECT_DIR}" # Variable substitution
env:
SOME_VAR: value
allowed_tools: # REQUIRED - explicit list of tools to allow
- read_file
- list_directory

# Org-level can block specific tools across ALL projects
blocked_mcp_tools:
- filesystem__write_file # Block write across all projects
- filesystem__delete_file
```

**Variable Substitution:**
- `${PROJECT_DIR}` - Absolute path to project directory
- `${HOME}` - User's home directory

**Tool Naming Convention:**
- MCP tools follow the pattern `mcp__{server}__{tool}`
- Config uses short names: `allowed_tools: [read_file]`
- Becomes: `mcp__filesystem__read_file`

**Key Behaviors:**
- `allowed_tools` is REQUIRED for each MCP server - must explicitly list tools
- Org `blocked_mcp_tools` takes precedence - projects cannot allow blocked tools
- Environment variables merge with parent environment
- Project can override org MCP server by using the same name

**Example Output:**

```
Created security settings at /path/to/project/.claude_settings.json
- MCP servers: playwright (browser), features (database), filesystem (custom)
- Blocked MCP tools (org): filesystem__write_file, filesystem__delete_file
```

### Vertex AI Configuration (Optional)

Run coding agents via Google Cloud Vertex AI:
Expand Down
94 changes: 92 additions & 2 deletions client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@
import shutil
import sys
from pathlib import Path
from typing import Any

from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
from claude_agent_sdk.types import HookContext, HookInput, HookMatcher, SyncHookJSONOutput
from dotenv import load_dotenv

from env_constants import API_ENV_VARS
from security import SENSITIVE_DIRECTORIES, bash_security_hook
from security import (
SENSITIVE_DIRECTORIES,
bash_security_hook,
get_effective_mcp_servers,
get_effective_mcp_tools,
)

# Load environment variables from .env file if present
load_dotenv()
Expand Down Expand Up @@ -182,6 +188,37 @@ def get_extra_read_paths() -> list[Path]:
return validated_paths


def substitute_variables(value: str, project_dir: Path) -> str:
"""
Substitute variables in a configuration value.

Supported variables:
- ${PROJECT_DIR} - Absolute path to project directory
- ${HOME} - User's home directory

Args:
value: String value that may contain variables
project_dir: Path to the project directory

Returns:
String with variables substituted
"""
result = value
result = result.replace("${PROJECT_DIR}", str(project_dir.resolve()))
result = result.replace("${HOME}", str(Path.home()))
return result


def substitute_variables_in_list(values: list[str], project_dir: Path) -> list[str]:
"""Substitute variables in a list of strings."""
return [substitute_variables(v, project_dir) for v in values]


def substitute_variables_in_dict(values: dict[str, str], project_dir: Path) -> dict[str, str]:
"""Substitute variables in a dict of strings."""
return {k: substitute_variables(v, project_dir) for k, v in values.items()}


# Per-agent-type MCP tool lists.
# Only expose the tools each agent type actually needs, reducing tool schema
# overhead and preventing agents from calling tools meant for other roles.
Expand Down Expand Up @@ -327,12 +364,21 @@ def create_client(
}
max_turns = max_turns_map.get(agent_type, 300)

# Load user-configured MCP servers from org and project configs
user_mcp_servers, blocked_mcp_tools = get_effective_mcp_servers(project_dir)
user_mcp_tools, user_mcp_permissions = get_effective_mcp_tools(
user_mcp_servers, blocked_mcp_tools
)

# Build allowed tools list based on mode and agent type.
# In YOLO mode, exclude Playwright tools for faster prototyping.
allowed_tools = [*BUILTIN_TOOLS, *feature_tools]
if not yolo_mode:
allowed_tools.extend(PLAYWRIGHT_TOOLS)

# Add user-configured MCP tools
allowed_tools.extend(user_mcp_tools)

# Build permissions list.
# We permit ALL feature MCP tools at the security layer (so the MCP server
# can respond if called), but the LLM only *sees* the agent-type-specific
Expand Down Expand Up @@ -367,6 +413,9 @@ def create_client(
# Allow Playwright MCP tools for browser automation (standard mode only)
permissions_list.extend(PLAYWRIGHT_TOOLS)

# Add user-configured MCP tool permissions
permissions_list.extend(user_mcp_permissions)

# Create comprehensive security settings
# Note: Using relative paths ("./**") restricts access to project directory
# since cwd is set to project_dir
Expand Down Expand Up @@ -394,10 +443,22 @@ def create_client(
if extra_read_paths:
print(f" - Extra read paths (validated): {', '.join(str(p) for p in extra_read_paths)}")
print(" - Bash commands restricted to allowlist (see security.py)")
if yolo_mode:

# Report MCP servers
builtin_servers = ["features (database)"]
if not yolo_mode:
builtin_servers.insert(0, "playwright (browser)")
user_server_names = [s["name"] for s in user_mcp_servers]
if user_server_names:
all_servers = builtin_servers + [f"{name} (custom)" for name in user_server_names]
print(f" - MCP servers: {', '.join(all_servers)}")
elif yolo_mode:
print(" - MCP servers: features (database) - YOLO MODE (no Playwright)")
else:
print(" - MCP servers: playwright (browser), features (database)")

if blocked_mcp_tools:
print(f" - Blocked MCP tools (org): {', '.join(sorted(blocked_mcp_tools))}")
print(" - Project settings enabled (skills, commands, CLAUDE.md)")
print()

Expand Down Expand Up @@ -448,6 +509,35 @@ def create_client(
"args": playwright_args,
}

# Add user-configured MCP servers from org and project configs
for server_config in user_mcp_servers:
server_name = server_config["name"]

# Skip if server name conflicts with built-in servers
if server_name in mcp_servers:
print(f" - Warning: Custom MCP server '{server_name}' conflicts with built-in, skipping")
continue

# Build server entry with variable substitution
server_entry: dict[str, Any] = {
"command": substitute_variables(server_config["command"], project_dir),
}

# Add args with variable substitution
if "args" in server_config:
server_entry["args"] = substitute_variables_in_list(
server_config["args"], project_dir
)

# Add env with variable substitution, merging with parent environment
if "env" in server_config:
server_entry["env"] = substitute_variables_in_dict(
server_config["env"], project_dir
)

Comment on lines +532 to +537
Copy link

@coderabbitai coderabbitai bot Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine client.py to understand the context
fd -t f "client.py" | head -20

Repository: leonvanzyl/autocoder

Length of output: 88


🏁 Script executed:

# Once found, let's examine the relevant section with more context
cat -n client.py | sed -n '520,550p'

Repository: leonvanzyl/autocoder

Length of output: 1468


🌐 Web query:

Claude Agent SDK MCP environment variables merge parent environment

💡 Result:

In the Claude Agent SDK, the env objects you set:

  • at the query level (options.env, e.g. ENABLE_TOOL_SEARCH) and
  • per MCP stdio server (options.mcpServers.<name>.env, e.g. GITHUB_TOKEN)

are intended to be additive overrides on top of the runtime’s existing environment (so you generally don’t need to restate things like PATH; you just set/override the specific variables you need). The Agent SDK docs show using env specifically to pass credentials and feature flags this way. [1]

Notes / gotchas:

  • Claude Code may still adjust some variables (people have reported extra path segments being prepended rather than the environment being replaced wholesale). [2]
  • In some Claude Desktop/Claude Code integration scenarios, there have been bugs where variables from an env section were not passed through to MCP servers as expected. [3]

Sources:
[1] Anthropic “Connect to external tools with MCP” (Agent SDK) docs https://platform.claude.com/docs/en/agent-sdk/mcp
[2] anthropics/claude-code issue #5202 (env vars like PATH/PYTHONPATH not preserved exactly) anthropics/claude-code#5202
[3] anthropics/claude-code issue #1254 (env vars in config not passed to MCP servers) anthropics/claude-code#1254


🌐 Web query:

MCP Model Context Protocol environment configuration SDK implementation

💡 Result:

MCP environment configuration (what to implement)

1) Configure MCP servers + per-server environment (.mcp.json)

In Claude/Agent SDK flows, MCP servers are commonly declared in a project-root .mcp.json, and each server can receive an env object (passed to the MCP server process). Example shown in the Claude docs: env: { "ALLOWED_PATHS": "..." }. [1]

2) Allow shared configs with environment-variable expansion

If you want portable configs (no secrets committed), implement/expect ${VAR} and ${VAR:-default} expansion in .mcp.json fields including command, args, env, url, and headers (Claude Code supports this). [2]

3) SDK-side “in code” configuration (instead of a file)

Claude’s Agent SDK allows passing MCP server definitions directly via an options.mcpServers object when calling query(). [1]

4) Application environment loading (Node/TS example)

A typical Node implementation loads environment variables (e.g., from .env) before constructing clients/transports (the MCP Node client tutorial uses dotenv.config() and reads ANTHROPIC_API_KEY from .env). [3]


Minimal reference implementations

A) .mcp.json (stdio server with env)

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem"],
      "env": { "ALLOWED_PATHS": "/Users/me/projects" }
    }
  }
}

[1]

B) .mcp.json with env-var expansion (remote HTTP/SSE auth)

{
  "mcpServers": {
    "api-server": {
      "type": "http",
      "url": "${API_BASE_URL:-https://api.example.com}/mcp",
      "headers": { "Authorization": "Bearer ${API_KEY}" }
    }
  }
}

[2]

C) Node client setup (dotenv + MCP TS SDK transport)

import dotenv from "dotenv";
dotenv.config(); // loads ANTHROPIC_API_KEY from .env

[3]


SDKs you can base your implementation on

  • Official MCP Swift SDK (client + server, transports, spec-versioned). [4]

Sources: [1] Claude Agent SDK “MCP in the SDK” docs, [2] Claude Code MCP docs (env expansion), [3] modelcontextprotocol.io Node client tutorial, [4] modelcontextprotocol/swift-sdk README.


🏁 Script executed:

# Search for where mcp_servers is used after construction
cat -n client.py | sed -n '539,600p'

Repository: leonvanzyl/autocoder

Length of output: 3494


🏁 Script executed:

# Look for SDK initialization and how mcp_servers is passed
rg -n "mcp_servers" client.py -A 3 -B 3

Repository: leonvanzyl/autocoder

Length of output: 2911


🏁 Script executed:

# Check if there are any imports or SDK references that show how env is handled
head -100 client.py | cat -n

Repository: leonvanzyl/autocoder

Length of output: 4333


Update comment accuracy and apply defensive env merge to handle known SDK issues.

The comment at line 533 claims "merging with parent environment" but the code only performs variable substitution without actually merging. The Claude Agent SDK is documented to treat env as additive overrides, however there are known issues in the SDK (#5202, #1254) where environment variables from MCP server configs are not correctly passed through. To ensure custom servers receive required variables like PATH, apply the defensive merge:

Fix: Explicitly merge env with parent
        if "env" in server_config:
-            server_entry["env"] = substitute_variables_in_dict(
-                server_config["env"], project_dir
-            )
+            server_entry["env"] = {
+                **os.environ,
+                **substitute_variables_in_dict(server_config["env"], project_dir),
+            }

Also update the comment on line 533 to: # Add env with variable substitution (defensive merge with parent)

🤖 Prompt for AI Agents
In `@client.py` around lines 533 - 538, Update the comment to "# Add env with
variable substitution (defensive merge with parent)"; then defensively merge the
substituted env into the existing parent env instead of replacing it — call
substitute_variables_in_dict(server_config["env"], project_dir) to get
substituted, obtain the parent env from server_entry.get("env",
os.environ.copy()) (import os if not already imported), update a copy of that
parent with the substituted values (so substituted values override), and assign
the merged dict back to server_entry["env"]; reference server_config,
server_entry, substitute_variables_in_dict, and project_dir when making the
change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leonvanzyl would you be ok with this suggest change?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

mcp_servers[server_name] = server_entry
print(f" - Custom MCP server '{server_name}': {server_config['command']}")

# Build environment overrides for API endpoint configuration
# These override system env vars for the Claude CLI subprocess,
# allowing AutoCoder to use alternative APIs (e.g., GLM) without
Expand Down
72 changes: 72 additions & 0 deletions examples/org_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,78 @@ blocked_commands: []
# - puppet


# ==========================================
# Custom MCP Servers
# ==========================================
# Configure additional MCP (Model Context Protocol) servers that will be
# available to the agent in ALL projects.
#
# Each MCP server MUST specify:
# - name: Unique identifier for the server
# - command: Command to run (e.g., "npx", "python")
# - allowed_tools: REQUIRED list of tools to allow (explicit allowlist)
#
# Optional fields:
# - args: List of command arguments
# - env: Environment variables (merged with parent environment)
#
# Variable substitution is supported:
# - ${PROJECT_DIR} - Absolute path to current project directory
# - ${HOME} - User's home directory
#
# By default, no custom MCP servers are configured.

mcp_servers: []

# Example: Filesystem MCP server (read-only)
# - name: filesystem
# command: npx
# args:
# - "@anthropic/mcp-server-filesystem"
# - "${PROJECT_DIR}"
# allowed_tools:
# - read_file
# - list_directory
# - search_files

# Example: Memory MCP server for persistent context
# - name: memory
# command: npx
# args:
# - "@anthropic/mcp-server-memory"
# env:
# MEMORY_DIR: "${HOME}/.autocoder/memory"
# allowed_tools:
# - store_memory
# - retrieve_memory
# - search_memory


# ==========================================
# Blocked MCP Tools (Organization-Wide)
# ==========================================
# Tools listed here are BLOCKED across ALL projects.
# Projects CANNOT override these blocks.
#
# Tool naming convention: {server}__{tool} or mcp__{server}__{tool}
# Examples:
# - filesystem__write_file (blocks filesystem server's write_file tool)
# - filesystem__delete_file (blocks filesystem server's delete_file tool)
# - myserver__* (blocks ALL tools from myserver - wildcard)
#
# By default, no tools are blocked.

blocked_mcp_tools: []

# Block dangerous filesystem operations
# - filesystem__write_file
# - filesystem__delete_file
# - filesystem__move_file

# Block all tools from a specific server (wildcard)
# - dangerous_server__*


# ==========================================
# Global Settings (Phase 3 feature)
# ==========================================
Expand Down
47 changes: 47 additions & 0 deletions examples/project_allowed_commands.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,53 @@ commands: []
# description: Deploy to staging environment


# ==========================================
# Custom MCP Servers
# ==========================================
# Configure additional MCP (Model Context Protocol) servers for THIS PROJECT.
# These are added to any org-level MCP servers defined in ~/.autocoder/config.yaml
#
# Each MCP server MUST specify:
# - name: Unique identifier for the server
# - command: Command to run (e.g., "npx", "python")
# - allowed_tools: REQUIRED list of tools to allow (explicit allowlist)
#
# Optional fields:
# - args: List of command arguments
# - env: Environment variables (merged with parent environment)
#
# Variable substitution is supported:
# - ${PROJECT_DIR} - Absolute path to current project directory
# - ${HOME} - User's home directory
#
# By default, no custom MCP servers are configured.

mcp_servers: []

# Example: SQLite MCP server for this project's database
# - name: sqlite
# command: npx
# args:
# - "@anthropic/mcp-server-sqlite"
# - "${PROJECT_DIR}/data.db"
# allowed_tools:
# - read_query
# - list_tables
# - describe_table

# Example: Custom project-specific MCP server
# - name: my_project_tools
# command: python
# args:
# - "${PROJECT_DIR}/tools/mcp_server.py"
# env:
# PROJECT_ROOT: "${PROJECT_DIR}"
# allowed_tools:
# - build_project
# - run_tests
# - deploy_staging


# ==========================================
# Notes and Best Practices
# ==========================================
Expand Down
Loading
Loading