Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f25c15b
refactor: remove custom MCP firewall, metadata, and built-in MCP concept
jamesadevine Mar 8, 2026
79d54a6
feat: add MCP Gateway (MCPG) support with SafeOutputs HTTP server
jamesadevine Mar 8, 2026
8407d35
feat: rewrite base.yml template for MCPG integration
jamesadevine Mar 8, 2026
c48fe16
test: update compiler tests for MCPG migration
jamesadevine Mar 8, 2026
5efe1b0
docs: update AGENTS.md for MCPG migration
jamesadevine Mar 8, 2026
fac2f2d
fix: address review feedback on MCPG integration
jamesadevine Mar 8, 2026
0c3ed0b
fix: harden security per PR review feedback
jamesadevine Mar 8, 2026
0ffbf7c
refactor: improve code quality per PR review feedback
jamesadevine Mar 8, 2026
a86e1cc
fix: address round 3 PR review feedback
jamesadevine Mar 8, 2026
3a43cdd
fix: harden SafeOutputs HTTP server security
jamesadevine Mar 10, 2026
e5769bf
fix: improve MCPG reliability and diagnostics
jamesadevine Mar 10, 2026
0c24343
fix: improve SafeOutputs HTTP server robustness
jamesadevine Mar 10, 2026
461715e
fix: correct MCPG networking for Linux host mode
jamesadevine Mar 10, 2026
7cb1c25
fix: add MCPG client auth and generate API key early
jamesadevine Mar 10, 2026
f5da079
fix: minor code quality improvements from review
jamesadevine Mar 10, 2026
3672534
chore: merge main into devinejames/remove-mcp-wrapper
jamesadevine Mar 31, 2026
ffa258c
refactor: inline MCPG image URL in template
jamesadevine Apr 1, 2026
58d104f
fix: pipeline template and docs fixes for SafeOutputs/MCPG
jamesadevine Apr 1, 2026
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
279 changes: 102 additions & 177 deletions AGENTS.md

Large diffs are not rendered by default.

381 changes: 376 additions & 5 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ serde = { version = "1.0.228", features = ["derive"] }
serde_yaml = "0.9.34"
serde_json = "1.0.149"
schemars = "1.2"
rmcp = { version = "0.8.0", features = ["server", "transport-io"] }
rmcp = { version = "0.8.0", features = ["server", "transport-io", "transport-streamable-http-server"] }
percent-encoding = "2.3"
reqwest = { version = "0.12", features = ["json"] }
tempfile = "3"
Expand All @@ -22,5 +22,7 @@ log = "0.4"
env_logger = "0.11"
regex-lite = "0.1"
inquire = { version = "0.9.2", features = ["editor"] }
axum = { version = "0.8.8", features = ["tokio"] }
subtle = "2.6.1"
terminal_size = "0.4.3"
url = "2.5.8"
4,176 changes: 0 additions & 4,176 deletions mcp-metadata.json

This file was deleted.

3 changes: 3 additions & 0 deletions src/allowed_hosts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ pub static CORE_ALLOWED_HOSTS: &[&str] = &[
// ===== Agency / Copilot configuration =====
"config.edge.skype.com",
// Note: 168.63.129.16 (Azure DNS) is handled separately as it's an IP
// Note: host.docker.internal is NOT in CORE — it's always added by the
// standalone compiler in generate_allowed_domains (standalone always uses
// MCPG, which needs host access from the AWF container).
];

/// Hosts required by specific MCP servers.
Expand Down
56 changes: 21 additions & 35 deletions src/compile/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ use anyhow::{Context, Result};

use super::types::{FrontMatter, McpConfig, Repository, TriggerConfig};
use crate::fuzzy_schedule;
use crate::mcp_metadata::McpMetadataFile;

/// Check if an MCP name is a built-in (launched via agency mcp)
pub fn is_builtin_mcp(name: &str) -> bool {
let metadata = McpMetadataFile::bundled();
metadata.get(name).map(|m| m.builtin).unwrap_or(false)
/// Check if an MCP has a custom command (i.e., is not just a name-based reference).
/// All MCPs now require explicit command configuration — there are no built-in MCPs
/// in the copilot CLI.
pub fn is_custom_mcp(config: &McpConfig) -> bool {
matches!(config, McpConfig::WithOptions(opts) if opts.command.is_some())
}

/// Parse the markdown file and extract front matter and body
Expand Down Expand Up @@ -303,14 +303,9 @@ pub fn generate_copilot_params(front_matter: &FrontMatter) -> String {
allowed_tools.push(format!("shell({})", cmd));
}

let metadata = McpMetadataFile::bundled();
let mut disallowed_mcps: Vec<&str> = metadata.mcp_names();
disallowed_mcps.sort();

let mut params = Vec::new();

params.push(format!("--model {}", front_matter.engine.model()));
params.push("--disable-builtin-mcps".to_string());
params.push("--no-ask-user".to_string());

for tool in allowed_tools {
Expand All @@ -323,26 +318,6 @@ pub fn generate_copilot_params(front_matter: &FrontMatter) -> String {
}
}

for mcp in disallowed_mcps {
params.push(format!("--disable-mcp-server {}", mcp));
}

for (name, config) in &front_matter.mcp_servers {
let is_custom = matches!(config, McpConfig::WithOptions(opts) if opts.command.is_some());
if is_custom {
continue;
}

let is_enabled = match config {
McpConfig::Enabled(enabled) => *enabled,
McpConfig::WithOptions(_) => true,
};

if is_enabled {
params.push(format!("--mcp {}", name));
}
}

params.join(" ")
}

Expand Down Expand Up @@ -490,6 +465,14 @@ pub fn generate_header_comment(input_path: &std::path::Path) -> String {
)
}

/// Docker image and version for the MCP Gateway (gh-aw-mcpg).
/// Update this when upgrading to a new MCPG release.
/// See: https://github.com/github/gh-aw-mcpg/releases
pub const MCPG_VERSION: &str = "0.1.9";

/// Default port MCPG listens on inside the container (host network mode).
pub const MCPG_PORT: u16 = 80;

/// Generate source path for the execute command.
///
/// Returns a path using `{{ workspace }}` as the base, which gets resolved
Expand Down Expand Up @@ -779,7 +762,7 @@ mod tests {
}

#[test]
fn test_copilot_params_custom_mcp_not_added_with_mcp_flag() {
fn test_copilot_params_custom_mcp_not_in_params() {
let mut fm = minimal_front_matter();
fm.mcp_servers.insert(
"my-tool".to_string(),
Expand All @@ -789,17 +772,20 @@ mod tests {
}),
);
let params = generate_copilot_params(&fm);
// Custom MCPs (with command) should NOT appear as --mcp flags
assert!(!params.contains("--mcp my-tool"));
// MCPs are handled by MCPG, not copilot CLI params
assert!(!params.contains("my-tool"));
}

#[test]
fn test_copilot_params_builtin_mcp_added_with_mcp_flag() {
fn test_copilot_params_no_mcp_flags() {
let mut fm = minimal_front_matter();
fm.mcp_servers
.insert("ado".to_string(), McpConfig::Enabled(true));
let params = generate_copilot_params(&fm);
assert!(params.contains("--mcp ado"));
// No --mcp or --disable-mcp-server flags — MCPs are handled by MCPG
assert!(!params.contains("--mcp"));
assert!(!params.contains("--disable-mcp-server"));
assert!(!params.contains("--disable-builtin-mcps"));
}

// ─── sanitize_filename ────────────────────────────────────────────────────
Expand Down
35 changes: 29 additions & 6 deletions src/compile/onees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use super::common::{
generate_ci_trigger, generate_copilot_ado_env, generate_executor_ado_env,
generate_header_comment, generate_pipeline_path, generate_pipeline_resources,
generate_pr_trigger, generate_repositories, generate_schedule, generate_source_path,
generate_working_directory, replace_with_indent, validate_comment_target,
generate_working_directory, is_custom_mcp, replace_with_indent, validate_comment_target,
validate_update_work_item_target, validate_write_permissions,
};
use super::types::{FrontMatter, McpConfig};
Expand Down Expand Up @@ -212,24 +212,47 @@ fn generate_agent_context_root(effective_workspace: &str) -> String {
}
}

/// Generate MCP configuration for 1ES templates
/// Generate MCP configuration for 1ES templates.
///
/// In 1ES, MCPs require service connections. Only MCPs with explicit
/// `service_connection` configuration or custom commands are included.
fn generate_mcp_configuration(mcps: &HashMap<String, McpConfig>) -> String {
let mut mcp_entries: Vec<_> = mcps
.iter()
.filter_map(|(name, config)| {
let (is_enabled, opts) = match config {
McpConfig::Enabled(enabled) => (*enabled, None),
McpConfig::WithOptions(o) => (o.command.is_none(), Some(o)), // Custom MCPs not supported
McpConfig::WithOptions(o) => (true, Some(o)),
};

if !is_enabled || !common::is_builtin_mcp(name) {
if !is_enabled {
return None;
}

// Use explicit service connection or generate default
// Custom MCPs with command: not supported in 1ES (needs service connection)
if is_custom_mcp(config) {
log::warn!(
"MCP '{}' uses custom command — not supported in 1ES target (requires service connection)",
name
);
return None;
}

// Use explicit service connection or generate default.
// Warn when falling back to the naming convention — the generated
// service connection reference may not exist in the ADO project.
let service_connection = opts
.and_then(|o| o.service_connection.clone())
.unwrap_or_else(|| format!("mcp-{}-service-connection", name));
.unwrap_or_else(|| {
let default = format!("mcp-{}-service-connection", name);
log::warn!(
"MCP '{}' has no explicit service connection in 1ES target — \
assuming '{}' exists",
name,
default,
);
default
});

Some((name.clone(), service_connection))
})
Expand Down
Loading
Loading