Skip to content

Commit 14fc372

Browse files
chambridgeclaude
andcommitted
feat(mcp): implement dynamic MCP servers configuration system
Add comprehensive MCP server configuration management for Claude Code runner: - Add dynamic MCP configuration loading from /app/.mcp.json with fallback to defaults - Create mcp-servers-configmap.yaml for Kubernetes ConfigMap integration - Update deploy.sh to support --atlassian-mcp-url parameter with SSE schema validation - Enhance startup logging to display loaded MCP servers before API key validation - Refactor _load_mcp_servers_config() as static method for cleaner architecture - Remove hardcoded "Playwright MCP" references to support generic MCP servers - Add proper SSE MCP server schema support: {"type": "sse", "url": "..."} This enables dynamic configuration of both command-based (playwright) and SSE-based (atlassian-mcp) MCP servers via Kubernetes ConfigMaps, improving flexibility and maintainability of the agentic runner system. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Chris Hambridge <chambrid@redhat.com>
1 parent 866ee3c commit 14fc372

File tree

5 files changed

+234
-21
lines changed

5 files changed

+234
-21
lines changed

components/manifests/deploy.sh

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/bash
22

33
# OpenShift Deployment Script for vTeam Ambient Agentic Runner
4-
# Usage: ./deploy.sh
4+
# Usage: ./deploy.sh [--atlassian-mcp-url=http://atlassian-mcp:8080/v1/sse]
55
# Or with environment variables: NAMESPACE=my-namespace ./deploy.sh
66
# Note: This script deploys pre-built images. Build and push images first.
77

@@ -26,6 +26,112 @@ DEFAULT_FRONTEND_IMAGE="${DEFAULT_FRONTEND_IMAGE:-quay.io/ambient_code/vteam_fro
2626
DEFAULT_OPERATOR_IMAGE="${DEFAULT_OPERATOR_IMAGE:-quay.io/ambient_code/vteam_operator:latest}"
2727
DEFAULT_RUNNER_IMAGE="${DEFAULT_RUNNER_IMAGE:-quay.io/ambient_code/vteam_claude_runner:latest}"
2828

29+
# Parse command line arguments
30+
ATLASSIAN_MCP_URL=""
31+
for arg in "$@"; do
32+
case $arg in
33+
--atlassian-mcp-url=*)
34+
ATLASSIAN_MCP_URL="${arg#*=}"
35+
shift
36+
;;
37+
uninstall)
38+
# Keep uninstall as a positional argument
39+
;;
40+
*)
41+
# Unknown option
42+
echo -e "${RED}❌ Unknown option: $arg${NC}"
43+
echo "Usage: $0 [--atlassian-mcp-url=http://atlassian-mcp:8080/v1/sse] [uninstall]"
44+
exit 1
45+
;;
46+
esac
47+
done
48+
49+
# Function to validate Atlassian MCP URL
50+
validate_atlassian_mcp_url() {
51+
local url="$1"
52+
53+
if [[ -z "$url" ]]; then
54+
return 0 # Empty URL is valid (optional parameter)
55+
fi
56+
57+
# Basic URL validation - must start with http or https
58+
if [[ ! "$url" =~ ^https?:// ]]; then
59+
echo -e "${RED}❌ Invalid Atlassian MCP URL format: $url${NC}"
60+
echo -e "${YELLOW}URL must start with http:// or https://${NC}"
61+
return 1
62+
fi
63+
64+
# Check if URL contains a valid domain or host
65+
if [[ ! "$url" =~ ^https?://[a-zA-Z0-9.-]+(:[0-9]+)?(/.*)?$ ]]; then
66+
echo -e "${RED}❌ Invalid Atlassian MCP URL format: $url${NC}"
67+
echo -e "${YELLOW}URL must be a valid MCP server endpoint${NC}"
68+
return 1
69+
fi
70+
71+
echo -e "${GREEN}✅ Atlassian MCP URL validation passed: $url${NC}"
72+
return 0
73+
}
74+
75+
# Validate Atlassian MCP URL if provided
76+
if [[ -n "$ATLASSIAN_MCP_URL" ]]; then
77+
if ! validate_atlassian_mcp_url "$ATLASSIAN_MCP_URL"; then
78+
exit 1
79+
fi
80+
fi
81+
82+
# Function to create MCP servers ConfigMap
83+
create_mcp_servers_configmap() {
84+
local namespace="$1"
85+
local atlassian_mcp_url="$2"
86+
87+
echo -e "${BLUE}Creating MCP servers ConfigMap...${NC}"
88+
89+
# Base configuration with playwright
90+
local mcp_config='{
91+
"mcpServers": {
92+
"playwright": {
93+
"command": "npx",
94+
"args": [
95+
"@playwright/mcp",
96+
"--headless",
97+
"--browser",
98+
"chromium",
99+
"--no-sandbox"
100+
]
101+
}'
102+
103+
# Add atlassian-mcp if MCP URL is provided
104+
if [[ -n "$atlassian_mcp_url" ]]; then
105+
echo -e "${YELLOW}Adding Atlassian MCP server for URL: ${atlassian_mcp_url}${NC}"
106+
mcp_config="$mcp_config"',
107+
"atlassian-mcp": {
108+
"type": "sse",
109+
"url": "'"$atlassian_mcp_url"'"
110+
}'
111+
fi
112+
113+
# Close the JSON structure
114+
mcp_config="$mcp_config"'
115+
}
116+
}'
117+
118+
# Create or update the ConfigMap
119+
if oc get configmap mcp-servers-config -n "$namespace" >/dev/null 2>&1; then
120+
echo -e "${YELLOW}Updating existing MCP servers ConfigMap...${NC}"
121+
oc delete configmap mcp-servers-config -n "$namespace"
122+
fi
123+
124+
oc create configmap mcp-servers-config \
125+
--from-literal=".mcp.json=$mcp_config" \
126+
-n "$namespace"
127+
128+
echo -e "${GREEN}✅ MCP servers ConfigMap created successfully${NC}"
129+
130+
if [[ -n "$atlassian_mcp_url" ]]; then
131+
echo -e "${YELLOW}Note: Ensure the atlassian-mcp service is deployed and accessible at ${atlassian_mcp_url}${NC}"
132+
fi
133+
}
134+
29135
# Handle uninstall command early
30136
if [ "${1:-}" = "uninstall" ]; then
31137
echo -e "${YELLOW}Uninstalling vTeam from namespace ${NAMESPACE}...${NC}"
@@ -72,6 +178,9 @@ echo -e "Backend Image: ${GREEN}${DEFAULT_BACKEND_IMAGE}${NC}"
72178
echo -e "Frontend Image: ${GREEN}${DEFAULT_FRONTEND_IMAGE}${NC}"
73179
echo -e "Operator Image: ${GREEN}${DEFAULT_OPERATOR_IMAGE}${NC}"
74180
echo -e "Runner Image: ${GREEN}${DEFAULT_RUNNER_IMAGE}${NC}"
181+
if [[ -n "$ATLASSIAN_MCP_URL" ]]; then
182+
echo -e "Atlassian MCP URL: ${GREEN}${ATLASSIAN_MCP_URL}${NC} (Atlassian MCP enabled)"
183+
fi
75184
echo ""
76185

77186
# Check prerequisites
@@ -168,6 +277,9 @@ kustomize edit set image quay.io/ambient_code/vteam_claude_runner:latest=${DEFAU
168277
echo -e "${BLUE}Building and applying manifests...${NC}"
169278
kustomize build . | oc apply -f -
170279

280+
# Create or update MCP servers ConfigMap with optional Atlassian MCP support
281+
create_mcp_servers_configmap "$NAMESPACE" "$ATLASSIAN_MCP_URL"
282+
171283
# Check if namespace exists and is active
172284
echo -e "${YELLOW}Checking namespace status...${NC}"
173285
if ! oc get namespace ${NAMESPACE} >/dev/null 2>&1; then
@@ -246,6 +358,11 @@ echo -e " ${BLUE}oc logs -f deployment/backend-api -n ${NAMESPACE}${NC}"
246358
echo -e " ${BLUE}oc logs -f deployment/agentic-operator -n ${NAMESPACE}${NC}"
247359
echo -e "4. Monitor RFE workflows:"
248360
echo -e " ${BLUE}oc get agenticsessions -n ${NAMESPACE}${NC}"
361+
if [[ -n "$ATLASSIAN_MCP_URL" ]]; then
362+
echo -e "5. Atlassian MCP server:"
363+
echo -e " ${YELLOW}Note: Atlassian MCP is configured but requires a separate deployment${NC}"
364+
echo -e " ${YELLOW}Ensure atlassian-mcp service is available at ${ATLASSIAN_MCP_URL}${NC}"
365+
fi
249366
echo ""
250367

251368
# Restore kustomization if we modified it

components/manifests/kustomization.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ resources:
1717
- rbac.yaml
1818
- secrets.yaml
1919
- git-configmap.yaml
20+
- mcp-servers-configmap.yaml
2021
- backend-deployment.yaml
2122
- frontend-deployment.yaml
2223
- operator-deployment.yaml
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: mcp-servers-config
5+
labels:
6+
app: ambient-code-runner
7+
component: configuration
8+
data:
9+
.mcp.json: |
10+
{
11+
"mcpServers": {
12+
"playwright": {
13+
"command": "npx",
14+
"args": [
15+
"@playwright/mcp",
16+
"--headless",
17+
"--browser",
18+
"chromium",
19+
"--no-sandbox"
20+
]
21+
}
22+
}
23+
}

components/operator/main.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error {
281281
// ⚠️ Let OpenShift SCC choose UID/GID dynamically (restricted-v2 compatible)
282282
// SecurityContext omitted to allow SCC assignment
283283

284-
// 🔧 Shared memory volume for browser and workspace storage for RFE workflows
284+
// 🔧 Shared memory volume for browser, workspace storage for RFE workflows, and MCP servers config
285285
Volumes: func() []corev1.Volume {
286286
volumes := []corev1.Volume{
287287
{
@@ -293,6 +293,22 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error {
293293
},
294294
},
295295
},
296+
{
297+
Name: "mcp-servers-config",
298+
VolumeSource: corev1.VolumeSource{
299+
ConfigMap: &corev1.ConfigMapVolumeSource{
300+
LocalObjectReference: corev1.LocalObjectReference{
301+
Name: "mcp-servers-config",
302+
},
303+
Items: []corev1.KeyToPath{
304+
{
305+
Key: ".mcp.json",
306+
Path: ".mcp.json",
307+
},
308+
},
309+
},
310+
},
311+
},
296312
}
297313

298314
// Add workspace PVC for RFE workflows if SHARED_WORKSPACE env var is set
@@ -324,10 +340,11 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error {
324340
},
325341
},
326342

327-
// 📦 Mount shared memory volume and workspace for RFE workflows
343+
// 📦 Mount shared memory volume, workspace for RFE workflows, and MCP servers config
328344
VolumeMounts: func() []corev1.VolumeMount {
329345
mounts := []corev1.VolumeMount{
330346
{Name: "dshm", MountPath: "/dev/shm"},
347+
{Name: "mcp-servers-config", MountPath: "/app/.mcp.json", SubPath: ".mcp.json"},
331348
}
332349

333350
// Add workspace mount for RFE workflows if SHARED_WORKSPACE env var is set

components/runners/claude-code-runner/main.py

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python3
22

33
import asyncio
4+
import json
45
import logging
56
import os
67
import requests
@@ -75,13 +76,64 @@ def __init__(self):
7576
logger.info(f"Workflow phase: {self.workflow_phase}")
7677
logger.info(f"Parent RFE: {self.parent_rfe}")
7778
logger.info(f"Website URL: {self.website_url}")
78-
logger.info("Using Claude Code CLI with Playwright MCP and spek-kit integration")
79+
logger.info("Using Claude Code CLI with MCP and spek-kit integration")
80+
81+
@staticmethod
82+
def _load_mcp_servers_config() -> Dict[str, Any]:
83+
"""Load MCP servers configuration from /app/.mcp.json with fallback to hardcoded default"""
84+
85+
# Default hardcoded configuration (existing setup)
86+
default_config = {
87+
"playwright": {
88+
"command": "npx",
89+
"args": [
90+
"@playwright/mcp",
91+
"--headless",
92+
"--browser",
93+
"chromium",
94+
"--no-sandbox",
95+
],
96+
}
97+
}
98+
99+
config_file_path = "/app/.mcp.json"
100+
101+
try:
102+
# Check if config file exists
103+
if not os.path.exists(config_file_path):
104+
logger.info(f"MCP config file {config_file_path} not found, using default configuration")
105+
return default_config
106+
107+
# Try to load and parse the JSON file
108+
with open(config_file_path, 'r') as f:
109+
config_data = json.load(f)
110+
111+
# Check if the loaded data has the required structure
112+
if not isinstance(config_data, dict) or "mcpServers" not in config_data:
113+
logger.warning(f"Invalid MCP config structure in {config_file_path}, missing 'mcpServers' key. Using default configuration")
114+
return default_config
115+
116+
mcp_servers = config_data["mcpServers"]
117+
if not isinstance(mcp_servers, dict):
118+
logger.warning(f"Invalid 'mcpServers' value in {config_file_path}, expected dict. Using default configuration")
119+
return default_config
120+
121+
logger.info(f"Successfully loaded MCP servers configuration from {config_file_path}")
122+
logger.info(f"Loaded MCP servers: {list(mcp_servers.keys())}")
123+
return mcp_servers
124+
125+
except json.JSONDecodeError as e:
126+
logger.error(f"Failed to parse JSON from {config_file_path}: {e}. Using default configuration")
127+
return default_config
128+
except Exception as e:
129+
logger.error(f"Error loading MCP config from {config_file_path}: {e}. Using default configuration")
130+
return default_config
79131

80132
async def run_agentic_session(self):
81133
"""Main method to run the agentic session"""
82134
try:
83135
logger.info(
84-
"Starting agentic session with Claude Code + Playwright MCP + spek-kit..."
136+
"Starting agentic session with Claude Code + MCP + spek-kit..."
85137
)
86138

87139
# Verify browser setup before starting
@@ -257,26 +309,15 @@ async def _run_claude_code(self, prompt: str) -> tuple[str, float, list[str]]:
257309
try:
258310
logger.info("Initializing Claude Code Python SDK with MCP server...")
259311

260-
# Configure MCP servers for OpenShift compatibility
261-
mcp_servers = {
262-
"playwright": {
263-
"command": "npx",
264-
"args": [
265-
"@playwright/mcp",
266-
"--headless",
267-
"--browser",
268-
"chromium",
269-
"--no-sandbox",
270-
],
271-
}
272-
}
312+
# Load MCP servers configuration from file or use default
313+
mcp_servers = ClaudeRunner._load_mcp_servers_config()
273314

274315
# Configure SDK with direct MCP server configuration
275316
options = ClaudeCodeOptions(
276-
system_prompt="You are an agentic assistant with browser automation capabilities via Playwright MCP tools.",
317+
system_prompt="You are an agentic assistant with browser automation capabilities via MCP tools.",
277318
max_turns=25,
278319
permission_mode="acceptEdits",
279-
allowed_tools=["mcp__playwright"],
320+
allowed_tools=["mcp__playwright", "mcp__atlassian-mcp"],
280321
mcp_servers=mcp_servers,
281322
cwd="/app",
282323
)
@@ -806,7 +847,21 @@ async def _handle_agent_rfe_session(self):
806847

807848
async def main():
808849
"""Main entry point"""
809-
logger.info("Claude Agentic Runner with Claude Code + Playwright MCP starting...")
850+
logger.info("Claude Agentic Runner with Claude Code starting...")
851+
852+
# Load and display MCP configuration early for verification (before API key check)
853+
logger.info("Loading MCP servers configuration for verification...")
854+
mcp_config = ClaudeRunner._load_mcp_servers_config()
855+
logger.info(f"MCP Configuration loaded with {len(mcp_config)} servers: {list(mcp_config.keys())}")
856+
857+
# Log detailed configuration for each server
858+
for server_name, config in mcp_config.items():
859+
if 'command' in config:
860+
logger.info(f" {server_name}: command={config['command']} args={config.get('args', [])}")
861+
elif 'url' in config:
862+
logger.info(f" {server_name}: url={config['url']}")
863+
else:
864+
logger.info(f" {server_name}: {config}")
810865

811866
# Validate required environment variables
812867
required_vars = [

0 commit comments

Comments
 (0)