Skip to content

Commit a46a85b

Browse files
feat: align MCP config with MCPG spec — container/HTTP transport (#157)
* feat: align MCP config with MCPG spec - container/HTTP transport Replace command-based MCP configuration with MCPG-native fields: - **types.rs**: Replace `command` with `container`, `entrypoint`, `entrypoint-args`, `url`, `headers`, `mounts` in McpOptions - **standalone.rs**: Update McpgServerConfig and generate_mcpg_config() to emit container/entrypointArgs (stdio) or url/headers (HTTP) - **standalone.rs**: Add generate_mcpg_docker_env() for env passthrough - Auto-maps SC_READ_TOKEN → AZURE_DEVOPS_EXT_PAT when permissions.read is configured - Forwards passthrough env vars ("") to MCPG via -e flags - **base.yml**: Add {{ mcpg_docker_env }} marker for env forwarding - **common.rs**: Update is_custom_mcp() to check container/url - **onees.rs**: Update custom MCP detection for 1ES target Tests: - New fixture: azure-devops-mcp-agent.md (container-based ADO MCP) - 4 new integration tests: fixture compilation, container config, HTTP config, env passthrough - 8 new unit tests: container/HTTP/entrypoint/mounts/env generation - Update existing tests for new field names Docs: - New example: examples/azure-devops-mcp.md (ADO work item triage) - Update AGENTS.md MCP section with container/HTTP docs and auth model Aligns with MCPG Gateway Specification v1.13.0 §3.2.1 (containerization requirement) and gh-aw front matter format. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address PR review — env var name validation, docs, HTTP env warning - Validate env var names against [A-Za-z_][A-Za-z0-9_]* to prevent Docker flag injection via user-controlled front matter keys - Document {{ mcpg_docker_env }} template marker in AGENTS.md - Warn when env: is configured on HTTP MCPs (silently ignored) - Add unit tests for env var name validation and injection rejection - Update {{ mcpg_config }} docs for container/url (was command) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: docker run line continuation bug, mount/URL validation - Fix broken bash line continuation when {{ mcpg_docker_env }} is non-empty or empty — restructured template so marker outputs include trailing backslash, validated with bash -n - Add validate_mount_source() — warns on sensitive host path prefixes (/etc, /root, /home, /proc, /sys, docker.sock) - Add validate_mcp_url() — warns if URL doesn't use http(s):// scheme Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: skip HTTP MCPs in env passthrough, warn on container+url conflict - Skip HTTP MCPs in generate_mcpg_docker_env (no child container to forward env vars to) - Warn when both container and url are set on same MCP entry - Escalate docker.sock mount warning to eprintln (container escape risk) - Remove undocumented ${VAR} passthrough — only empty string ("") triggers env passthrough, matching AGENTS.md documentation - Add indentation cross-reference comment for base.yml alignment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: validate Docker args, warn on inline secrets, consistent eprintln - Add validate_docker_args() — scans args for --privileged, -v, --cap-add, --security-opt and other privilege escalation flags - Add warn_potential_secrets() — warns when env var names containing 'token', 'secret', 'key', 'password', 'pat' have inline values instead of passthrough; warns on Authorization headers in plaintext - Change validate_mcp_url to use eprintln! for consistency with other compile-time warnings (was log::warn, invisible without --verbose) - Change HTTP MCP env warning to eprintln! for consistency Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: tighten validation, scope ADO token auto-map to requesting MCPs - Remove dead /var/run/docker.sock from SENSITIVE_MOUNT_PREFIXES (already caught by contains("docker.sock") early return) - Fix prefix check: /etc no longer matches /etc-configs (require exact match or trailing /) - Fix Docker args: handle split-form flags (--pid host, --network host, --ipc host) in addition to --flag=value form - Scope ADO token auto-map: only inject AZURE_DEVOPS_EXT_PAT when a container MCP actually requests it via env passthrough, not whenever permissions.read is set - Add test for dedup edge case: auto-mapped SC_READ_TOKEN form wins over bare passthrough, appears exactly once - Add test for no-request case: permissions.read without MCP requesting AZURE_DEVOPS_EXT_PAT does not inject the token Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: use replace_with_indent for mcpg_docker_env indentation Move {{ mcpg_docker_env }} to its own indented line in base.yml so replace_with_indent handles alignment automatically. Removes hardcoded 12-space indentation from generate_mcpg_docker_env() — future template reformats won't silently break bash syntax. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: container image validation, expand Docker flag list, fix headers arg - Add validate_container_image() — rejects image names with unexpected characters (defense-in-depth against injection) - Expand DANGEROUS_DOCKER_FLAGS with --user/-u, --add-host, --entrypoint - Fix warn_potential_secrets call: pass &opts.headers instead of empty HashMap for container MCPs - Add inline comment explaining lone backslash in empty env case - Add case-insensitivity note on validate_mount_source Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: warn on missing permissions.read when MCP requests ADO token - Emit eprintln warning when container MCP has AZURE_DEVOPS_EXT_PAT passthrough but permissions.read is not configured (token would be empty at runtime, causing silent auth failure) - Remove --network host from AGENTS.md args example (contradicts DANGEROUS_DOCKER_FLAGS warning, bypasses AWF proxy) - Add entrypoint: field hint to --entrypoint Docker args warning Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * style: simplify if-let Some(_) to .is_some() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3d7679c commit a46a85b

10 files changed

Lines changed: 1046 additions & 95 deletions

File tree

AGENTS.md

Lines changed: 95 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,10 @@ tools: # optional tool configuration
128128
# env: # RESERVED: workflow-level environment variables (not yet implemented)
129129
# CUSTOM_VAR: "value"
130130
mcp-servers:
131-
my-custom-tool: # custom MCP server (requires command field)
132-
command: "node"
133-
args: ["path/to/mcp-server.js"]
131+
my-custom-tool: # containerized MCP server (requires container field)
132+
container: "node:20-slim"
133+
entrypoint: "node"
134+
entrypoint-args: ["path/to/mcp-server.js"]
134135
allowed:
135136
- custom_function_1
136137
- custom_function_2
@@ -589,13 +590,25 @@ Should be replaced with the markdown body (agent instructions) extracted from th
589590
Should be replaced with the MCP Gateway (MCPG) configuration JSON generated from the `mcp-servers:` front matter. This configuration defines the MCPG server entries and gateway settings.
590591

591592
The generated JSON has two top-level sections:
592-
- `mcpServers`: Maps server names to their configuration (type, command/url, tools, etc.)
593+
- `mcpServers`: Maps server names to their configuration (type, container/url, tools, etc.)
593594
- `gateway`: Gateway settings (port, domain, apiKey, payloadDir)
594595

595-
SafeOutputs is always included as an HTTP backend (`type: "http"`) pointing to `localhost` (MCPG runs with `--network host`, so `localhost` is the host loopback). Custom MCPs with explicit `command:` are included as stdio servers (`type: "stdio"`). MCPs without a command are skipped (there are no built-in MCPs in the copilot CLI).
596+
SafeOutputs is always included as an HTTP backend (`type: "http"`) pointing to `localhost` (MCPG runs with `--network host`, so `localhost` is the host loopback). Containerized MCPs with `container:` are included as stdio servers (`type: "stdio"` with `container`, `entrypoint`, `entrypointArgs`). HTTP MCPs with `url:` are included as HTTP servers. MCPs without a container or url are skipped.
596597

597598
Runtime placeholders (`${SAFE_OUTPUTS_PORT}`, `${SAFE_OUTPUTS_API_KEY}`, `${MCP_GATEWAY_API_KEY}`) are substituted by the pipeline at runtime before passing the config to MCPG.
598599

600+
## {{ mcpg_docker_env }}
601+
602+
Should be replaced with additional `-e` flags for the MCPG Docker run command, enabling environment variable passthrough from the pipeline to MCP containers.
603+
604+
When `permissions.read` is configured, the compiler automatically adds `-e AZURE_DEVOPS_EXT_PAT="$(SC_READ_TOKEN)"` to forward the ADO access token to MCP containers that need it (e.g., Azure DevOps MCP).
605+
606+
Additionally, any env vars in MCP configs with empty string values (`""`) are collected and forwarded as `-e VAR_NAME` flags, enabling passthrough from the pipeline environment through MCPG to MCP child containers.
607+
608+
Environment variable names are validated against `[A-Za-z_][A-Za-z0-9_]*` to prevent Docker flag injection.
609+
610+
If no passthrough env vars are needed, this marker is replaced with an empty string.
611+
599612
## {{ allowed_domains }}
600613

601614
Should be replaced with the comma-separated domain list for AWF's `--allow-domains` flag. The list includes:
@@ -1129,61 +1142,100 @@ cargo add <crate-name>
11291142

11301143
## MCP Configuration
11311144

1132-
The `mcp-servers:` field configures MCP (Model Context Protocol) servers that are made available to the agent via the MCP Gateway (MCPG). All MCPs require explicit `command:` configuration — there are no built-in MCPs in the copilot CLI.
1133-
The `mcp-servers:` field configures custom MCP (Model Context Protocol) servers that the agent can use. Each entry must include a `command:` field specifying the executable to spawn.
1145+
The `mcp-servers:` field configures MCP (Model Context Protocol) servers that are made available to the agent via the MCP Gateway (MCPG). MCPs can be **containerized stdio servers** (Docker-based) or **HTTP servers** (remote endpoints). All MCP traffic flows through the MCP Gateway.
11341146

1135-
### Custom MCP Servers
1147+
### Docker Container MCP Servers (stdio)
11361148

1137-
Define MCP servers by including a `command:` field:
1149+
Run containerized MCP servers. MCPG spawns these as sibling Docker containers:
11381150

11391151
```yaml
11401152
mcp-servers:
1141-
my-custom-tool:
1142-
command: "node"
1143-
args: ["path/to/mcp-server.js"]
1153+
azure-devops:
1154+
container: "node:20-slim"
1155+
entrypoint: "npx"
1156+
entrypoint-args: ["-y", "@azure-devops/mcp", "myorg", "-d", "core", "work-items"]
1157+
env:
1158+
AZURE_DEVOPS_EXT_PAT: ""
11441159
allowed:
1145-
- custom_function_1
1146-
- custom_function_2
1160+
- core_list_projects
1161+
- wit_get_work_item
1162+
- wit_create_work_item
1163+
```
1164+
1165+
### HTTP MCP Servers (remote)
1166+
1167+
Connect to remote MCP servers accessible via HTTP:
1168+
1169+
```yaml
1170+
mcp-servers:
1171+
remote-ado:
1172+
url: "https://mcp.dev.azure.com/myorg"
1173+
headers:
1174+
X-MCP-Toolsets: "repos,wit"
1175+
X-MCP-Readonly: "true"
1176+
allowed:
1177+
- wit_get_work_item
1178+
- repo_list_repos_by_project
11471179
```
11481180

11491181
### Configuration Properties
11501182

1151-
- `command:` - The executable to run (e.g., `"node"`, `"python"`, `"dotnet"`)
1152-
- `args:` - Array of command-line arguments passed to the command
1153-
- `allowed:` - Array of function names agents are permitted to call (required for security)
1154-
- `env:` - Optional environment variables for the MCP server process
1155-
- `service-connection:` - (1ES target only) Override the service connection name used for this MCP. If not specified, defaults to `mcp-<name>-service-connection`
1183+
**Container stdio servers:**
1184+
- `container:` - Docker image to run (e.g., `"node:20-slim"`, `"ghcr.io/org/tool:latest"`)
1185+
- `entrypoint:` - Container entrypoint override (equivalent to `docker run --entrypoint`)
1186+
- `entrypoint-args:` - Arguments passed to the entrypoint (after the image in `docker run`)
1187+
- `args:` - Additional Docker runtime arguments (inserted before the image in `docker run`). **Security note**: dangerous flags like `--privileged`, `--network host` will trigger compile-time warnings.
1188+
- `mounts:` - Volume mounts in `"source:dest:mode"` format (e.g., `["/host/data:/app/data:ro"]`)
1189+
1190+
**HTTP servers:**
1191+
- `url:` - HTTP endpoint URL for the remote MCP server
1192+
- `headers:` - HTTP headers to include in requests (e.g., `Authorization`, `X-MCP-Toolsets`)
1193+
1194+
**Common (both types):**
1195+
- `allowed:` - Array of tool names the agent is permitted to call (required for security)
1196+
- `env:` - Environment variables for the MCP server process. Use `""` (empty string) for passthrough from the pipeline environment.
1197+
- `service-connection:` - (1ES target only) Override the service connection name. Defaults to `mcp-<name>-service-connection`
1198+
1199+
### Environment Variable Passthrough
1200+
1201+
MCP containers may need secrets from the pipeline (e.g., ADO tokens). The `env:` field supports passthrough:
1202+
1203+
```yaml
1204+
env:
1205+
AZURE_DEVOPS_EXT_PAT: "" # Passthrough from pipeline environment
1206+
STATIC_CONFIG: "some-value" # Literal value embedded in config
1207+
```
1208+
1209+
When `permissions.read` is configured, the compiler automatically maps `SC_READ_TOKEN` → `AZURE_DEVOPS_EXT_PAT` on the MCPG container, so agents can access ADO APIs without manual wiring.
11561210

1157-
### Example: Multiple Custom MCP Servers
1211+
### Example: Azure DevOps MCP with Authentication
11581212

11591213
```yaml
11601214
mcp-servers:
1161-
# Custom Python MCP server
1162-
data-processor:
1163-
command: "python"
1164-
args: ["-m", "my_mcp_server"]
1215+
azure-devops:
1216+
container: "node:20-slim"
1217+
entrypoint: "npx"
1218+
entrypoint-args: ["-y", "@azure-devops/mcp", "myorg"]
11651219
env:
1166-
DATA_DIR: "/data"
1167-
allowed:
1168-
- process_data
1169-
- query_database
1170-
1171-
# Custom .NET MCP server
1172-
azure-tools:
1173-
command: "dotnet"
1174-
args: ["./tools/AzureMcp.dll"]
1220+
AZURE_DEVOPS_EXT_PAT: ""
11751221
allowed:
1176-
- list_resources
1177-
- get_deployment_status
1222+
- core_list_projects
1223+
- wit_get_work_item
1224+
permissions:
1225+
read: my-read-arm-connection
1226+
network:
1227+
allow:
1228+
- "dev.azure.com"
1229+
- "*.dev.azure.com"
11781230
```
11791231

11801232
### Security Notes
11811233

1182-
1. **Allow-listing**: Only functions explicitly listed in `allowed:` are accessible to agents
1183-
2. **Command Validation**: The compiler validates that commands are from a trusted set
1184-
3. **Argument Sanitization**: Arguments are validated to prevent injection attacks
1185-
4. **Environment Isolation**: MCP servers run in the same isolated sandbox as the pipeline
1186-
5. **MCPG Gateway**: All MCP traffic flows through the MCP Gateway which enforces tool-level filtering
1234+
1. **Allow-listing**: Only tools explicitly listed in `allowed:` are accessible to agents
1235+
2. **Containerization**: Stdio MCP servers run as isolated Docker containers (per MCPG spec §3.2.1)
1236+
3. **Environment Isolation**: MCP containers are spawned by MCPG with only the configured environment variables
1237+
4. **MCPG Gateway**: All MCP traffic flows through the MCP Gateway which enforces tool-level filtering
1238+
5. **Network Isolation**: MCP containers run within the same AWF-isolated network. Users must explicitly allow external domains via `network.allow`
11871239

11881240
## Network Isolation (AWF)
11891241

@@ -1346,8 +1398,9 @@ The compiler generates MCPG configuration JSON from the `mcp-servers:` front mat
13461398
},
13471399
"custom-tool": {
13481400
"type": "stdio",
1349-
"command": "node",
1350-
"args": ["server.js"],
1401+
"container": "node:20-slim",
1402+
"entrypoint": "node",
1403+
"entrypointArgs": ["server.js"],
13511404
"tools": ["process_data", "get_status"]
13521405
}
13531406
},

examples/azure-devops-mcp.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
name: "ADO Work Item Triage"
3+
description: "Triages work items using the Azure DevOps MCP"
4+
schedule: daily around 9:00
5+
engine: claude-sonnet-4.5
6+
mcp-servers:
7+
azure-devops:
8+
container: "node:20-slim"
9+
entrypoint: "npx"
10+
entrypoint-args: ["-y", "@azure-devops/mcp", "myorg", "-d", "core", "work", "work-items"]
11+
env:
12+
AZURE_DEVOPS_EXT_PAT: ""
13+
allowed:
14+
- core_list_projects
15+
- wit_my_work_items
16+
- wit_get_work_item
17+
- wit_get_work_items_batch_by_ids
18+
- wit_update_work_item
19+
- wit_add_work_item_comment
20+
- search_workitem
21+
permissions:
22+
read: my-read-arm-connection
23+
write: my-write-arm-connection
24+
safe-outputs:
25+
update-work-item:
26+
status: true
27+
body: true
28+
tags: true
29+
max: 10
30+
target: "*"
31+
comment-on-work-item:
32+
max: 10
33+
target: "*"
34+
network:
35+
allow:
36+
- "dev.azure.com"
37+
- "*.dev.azure.com"
38+
- "*.visualstudio.com"
39+
---
40+
41+
## ADO Work Item Triage Agent
42+
43+
You have access to the Azure DevOps MCP server. Use it to:
44+
45+
1. Query open work items assigned to the team
46+
2. Triage items by priority and area path
47+
3. Add comments summarizing your analysis
48+
4. Update tags to reflect triage status
49+
50+
### Guidelines
51+
52+
- Only update work items that need attention
53+
- Add the `triaged` tag to items you've reviewed
54+
- Add a comment explaining your triage decision

src/compile/common.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ use super::types::{FrontMatter, Repository, TriggerConfig};
66
use crate::compile::types::McpConfig;
77
use crate::fuzzy_schedule;
88

9-
/// Check if an MCP has a custom command (i.e., is not just a name-based reference).
10-
/// All MCPs now require explicit command configuration — there are no built-in MCPs
11-
/// in the copilot CLI.
9+
/// Check if an MCP has a transport configuration (container or URL).
10+
/// MCPs with a container are containerized stdio servers; MCPs with a URL
11+
/// are HTTP servers. Both are routed through the MCP Gateway (MCPG).
1212
pub fn is_custom_mcp(config: &McpConfig) -> bool {
13-
matches!(config, McpConfig::WithOptions(opts) if opts.command.is_some())
13+
matches!(config, McpConfig::WithOptions(opts) if opts.container.is_some() || opts.url.is_some())
1414
}
1515

1616
/// Parse the markdown file and extract front matter and body
@@ -1071,7 +1071,7 @@ mod tests {
10711071
fm.mcp_servers.insert(
10721072
"my-tool".to_string(),
10731073
McpConfig::WithOptions(McpOptions {
1074-
command: Some("node".to_string()),
1074+
container: Some("node:20-slim".to_string()),
10751075
..Default::default()
10761076
}),
10771077
);

src/compile/onees.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,10 @@ displayName: "Finalize""#,
214214
if front_matter
215215
.mcp_servers
216216
.iter()
217-
.any(|(_, c)| matches!(c, McpConfig::WithOptions(o) if o.command.is_some()))
217+
.any(|(_, c)| is_custom_mcp(c))
218218
{
219219
eprintln!(
220-
"Warning: Custom MCP servers (with command:) are not supported in 1ES target. \
220+
"Warning: Custom MCP servers (with container: or url:) are not supported in 1ES target. \
221221
They will be ignored. Use standalone target for full MCP support."
222222
);
223223
}
@@ -257,10 +257,10 @@ fn generate_mcp_configuration(mcps: &HashMap<String, McpConfig>) -> String {
257257
return None;
258258
}
259259

260-
// Custom MCPs with command: not supported in 1ES (needs service connection)
260+
// Custom MCPs with container/url: not supported in 1ES (needs service connection)
261261
if is_custom_mcp(config) {
262262
log::warn!(
263-
"MCP '{}' uses custom command — not supported in 1ES target (requires service connection)",
263+
"MCP '{}' uses custom container/url — not supported in 1ES target (requires service connection)",
264264
name
265265
);
266266
return None;

0 commit comments

Comments
 (0)