Skip to content

Commit a1fa9ff

Browse files
markturanskyuserclaude
authored
feat(runner): dynamic credential-aware MCP server configuration (#1593)
## Summary - **Remove `acp` backend MCP server** from `build_mcp_servers()` — the 9 `acp_*` tools wrapping the legacy K8s backend are superseded by the `ambient` MCP sidecar's 15 tools - **Add `build_credential_mcp_servers()`** — reads `CREDENTIAL_IDS` env var and dynamically configures MCP servers only for credentials actually bound to the project (`jira` → `mcp-atlassian`, `kubeconfig` → `openshift`, `google` → `google-workspace`) - **Remove credential-bearing entries from `.mcp.json`** — `mcp-atlassian`, `openshift`, `google-workspace` were previously loaded unconditionally; now they're only provisioned when the corresponding credential is bound. Credential-free utilities (`context7`, `deepwiki`, `webfetch`) are retained. ## Motivation Previously, credential-bearing MCP servers were always loaded from `.mcp.json` regardless of whether credentials were bound, leading to startup failures and unnecessary MCP processes. This change makes MCP server provisioning credential-aware: only servers whose credentials are actually available get configured. ## Test plan - [x] 549 runner tests pass (152 MCP-specific tests pass) - [ ] Verify `mcp-atlassian` appears in MCP servers when `jira` credential is bound - [ ] Verify `openshift` appears when `kubeconfig` credential is bound - [ ] Verify `google-workspace` appears when `google` credential is bound - [ ] Verify no credential MCP servers appear when `CREDENTIAL_IDS` is empty - [ ] Verify `ambient` MCP sidecar still works via SSE 🤖 Generated with [Claude Code](https://claude.ai/code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Updates** * Removed built-in support for Atlassian, OpenShift, and Google Workspace integrations; remaining default servers are limited. * Added credential-aware support for Jira, Kubeconfig, and Google services that can be enabled via credentials. * **Bug Fixes / Resilience** * Installer step now tolerates CodeRabbit CLI install failures and logs a warning instead of failing the build. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: user <u@example.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 63545c7 commit a1fa9ff

3 files changed

Lines changed: 98 additions & 52 deletions

File tree

components/runners/ambient-runner/.mcp.json

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,45 +13,6 @@
1313
"args": [
1414
"mcp-server-fetch"
1515
]
16-
},
17-
"mcp-atlassian": {
18-
"command": "uvx",
19-
"args": [
20-
"mcp-atlassian"
21-
],
22-
"env": {
23-
"JIRA_URL": "${JIRA_URL}",
24-
"JIRA_USERNAME": "${JIRA_EMAIL}",
25-
"JIRA_API_TOKEN": "${JIRA_API_TOKEN}",
26-
"JIRA_SSL_VERIFY": "true",
27-
"READ_ONLY_MODE": "${JIRA_READ_ONLY_MODE:-true}"
28-
}
29-
},
30-
"openshift": {
31-
"command": "uvx",
32-
"args": [
33-
"kubernetes-mcp-server@latest",
34-
"--kubeconfig",
35-
"/tmp/.ambient_kubeconfig",
36-
"--disable-multi-cluster"
37-
]
38-
},
39-
"google-workspace": {
40-
"command": "uvx",
41-
"args": [
42-
"workspace-mcp@1.17.1",
43-
"--permissions",
44-
"gmail:send",
45-
"drive:full"
46-
],
47-
"env": {
48-
"GOOGLE_MCP_CREDENTIALS_DIR": "${GOOGLE_MCP_CREDENTIALS_DIR}",
49-
"MCP_SINGLE_USER_MODE": "1",
50-
"GOOGLE_OAUTH_CLIENT_ID": "${GOOGLE_OAUTH_CLIENT_ID}",
51-
"GOOGLE_OAUTH_CLIENT_SECRET": "${GOOGLE_OAUTH_CLIENT_SECRET}",
52-
"GOOGLE_OAUTH_REDIRECT_URI": "${GOOGLE_OAUTH_REDIRECT_URI}",
53-
"USER_GOOGLE_EMAIL": "${USER_GOOGLE_EMAIL:-user@example.com}"
54-
}
5516
}
5617
}
5718
}

components/runners/ambient-runner/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ RUN npm install -g @google/gemini-cli@${GEMINI_CLI_VERSION} && \
4444
npm cache clean --force
4545

4646
# Install CodeRabbit CLI (official install script, binary for current arch)
47-
RUN curl -fsSL https://cli.coderabbit.ai/install.sh | CODERABBIT_INSTALL_DIR=/usr/local/bin sh
47+
RUN curl -fsSL https://cli.coderabbit.ai/install.sh | CODERABBIT_INSTALL_DIR=/usr/local/bin sh \
48+
|| echo "WARNING: CodeRabbit CLI install failed — cli.coderabbit.ai may be down; continuing without it"
4849

4950
# Set environment variables
5051
ENV PYTHONUNBUFFERED=1

components/runners/ambient-runner/ambient_runner/bridges/claude/mcp.py

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ def build_mcp_servers(
6262
load_rubric_content,
6363
)
6464
from ambient_runner.bridges.claude.corrections import create_correction_mcp_tool
65-
from ambient_runner.bridges.claude.backend_tools import create_backend_mcp_tools
6665

6766
mcp_servers = load_mcp_config(context, cwd_path) or {}
6867

@@ -118,18 +117,12 @@ def build_mcp_servers(
118117
mcp_servers["corrections"] = correction_server
119118
logger.info("Added corrections feedback MCP tool (log_correction)")
120119

121-
# Backend API tools (session management)
122-
backend_tools = create_backend_mcp_tools(sdk_tool_decorator=sdk_tool)
123-
if backend_tools:
124-
backend_server = create_sdk_mcp_server(
125-
name="acp", version="1.0.0", tools=backend_tools
126-
)
127-
mcp_servers["acp"] = backend_server
120+
# Credential-aware MCP servers (dynamically configured per bound credentials)
121+
credential_mcp_servers = build_credential_mcp_servers()
122+
mcp_servers.update(credential_mcp_servers)
123+
if credential_mcp_servers:
128124
logger.info(
129-
f"Added backend API MCP tools ({len(backend_tools)}): "
130-
"acp_list_sessions, acp_get_session, acp_create_session, "
131-
"acp_stop_session, acp_send_message, acp_get_session_status, "
132-
"acp_restart_session, acp_list_workflows, acp_get_api_reference"
125+
"Added credential MCP servers: %s", list(credential_mcp_servers.keys())
133126
)
134127

135128
# Gerrit MCP server (only if credentials are configured)
@@ -148,6 +141,97 @@ def build_mcp_servers(
148141
return mcp_servers
149142

150143

144+
_CREDENTIAL_MCP_REGISTRY: dict[str, dict] = {
145+
"jira": {
146+
"command": "uvx",
147+
"args": ["mcp-atlassian"],
148+
"env": {
149+
"JIRA_URL": "${JIRA_URL}",
150+
"JIRA_USERNAME": "${JIRA_EMAIL}",
151+
"JIRA_API_TOKEN": "${JIRA_API_TOKEN}",
152+
"JIRA_SSL_VERIFY": "true",
153+
"READ_ONLY_MODE": "${JIRA_READ_ONLY_MODE:-true}",
154+
},
155+
"server_name": "mcp-atlassian",
156+
},
157+
"kubeconfig": {
158+
"command": "uvx",
159+
"args": [
160+
"kubernetes-mcp-server@latest",
161+
"--kubeconfig",
162+
"/tmp/.ambient_kubeconfig",
163+
"--disable-multi-cluster",
164+
],
165+
"server_name": "openshift",
166+
},
167+
"google": {
168+
"command": "uvx",
169+
"args": ["workspace-mcp@1.17.1", "--permissions", "gmail:send", "drive:full"],
170+
"env": {
171+
"GOOGLE_MCP_CREDENTIALS_DIR": "${GOOGLE_MCP_CREDENTIALS_DIR}",
172+
"MCP_SINGLE_USER_MODE": "1",
173+
"GOOGLE_OAUTH_CLIENT_ID": "${GOOGLE_OAUTH_CLIENT_ID}",
174+
"GOOGLE_OAUTH_CLIENT_SECRET": "${GOOGLE_OAUTH_CLIENT_SECRET}",
175+
"GOOGLE_OAUTH_REDIRECT_URI": "${GOOGLE_OAUTH_REDIRECT_URI}",
176+
"USER_GOOGLE_EMAIL": "${USER_GOOGLE_EMAIL:-user@example.com}",
177+
},
178+
"server_name": "google-workspace",
179+
},
180+
}
181+
182+
183+
def _expand_env_vars(value: str) -> str:
184+
import re as _re
185+
186+
def _replace(match: _re.Match) -> str:
187+
expr = match.group(1)
188+
if ":-" in expr:
189+
var, default = expr.split(":-", 1)
190+
return os.environ.get(var, default)
191+
return os.environ.get(expr, match.group(0))
192+
193+
return _re.sub(r"\$\{([^}]+)}", _replace, value)
194+
195+
196+
def build_credential_mcp_servers() -> dict:
197+
credential_ids_raw = os.getenv("CREDENTIAL_IDS", "").strip()
198+
if not credential_ids_raw:
199+
return {}
200+
201+
try:
202+
credential_ids = json.loads(credential_ids_raw)
203+
except (json.JSONDecodeError, TypeError):
204+
logger.warning("Failed to parse CREDENTIAL_IDS — skipping credential MCP servers")
205+
return {}
206+
207+
servers: dict = {}
208+
for provider, _cred_id in credential_ids.items():
209+
registry_entry = _CREDENTIAL_MCP_REGISTRY.get(provider)
210+
if not registry_entry:
211+
continue
212+
213+
server_name = registry_entry["server_name"]
214+
server_config: dict = {
215+
"command": registry_entry["command"],
216+
"args": list(registry_entry["args"]),
217+
}
218+
219+
if "env" in registry_entry:
220+
server_config["env"] = {
221+
k: _expand_env_vars(v) for k, v in registry_entry["env"].items()
222+
}
223+
224+
servers[server_name] = server_config
225+
logger.info(
226+
"Configured %s MCP server for bound credential %s (provider: %s)",
227+
server_name,
228+
_cred_id,
229+
provider,
230+
)
231+
232+
return servers
233+
234+
151235
def build_allowed_tools(mcp_servers: dict) -> list[str]:
152236
"""Build the list of allowed tool names from default tools + MCP servers."""
153237
allowed = list(DEFAULT_ALLOWED_TOOLS)

0 commit comments

Comments
 (0)