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
279 changes: 102 additions & 177 deletions AGENTS.md

Large diffs are not rendered by default.

385 changes: 372 additions & 13 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ 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"] }
reqwest = { version = "0.12", features = ["json"] }
tempfile = "3"
tokio = { version = "1.43", features = ["full"] }
log = "0.4"
env_logger = "0.11"
regex-lite = "0.1"
inquire = { version = "0.9.2", features = ["editor"] }
terminal_size = "0.4.3"
axum = { version = "0.8.8", features = ["tokio"] }
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
57 changes: 22 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 @@ -306,14 +306,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 @@ -326,26 +321,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 @@ -460,6 +435,15 @@ pub const DEFAULT_POOL: &str = "AZS-1ES-L-MMS-ubuntu-22.04";
/// See: https://github.com/github/gh-aw-firewall/releases
pub const AWF_VERSION: &str = "0.23.1";

/// 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";
pub const MCPG_IMAGE: &str = "ghcr.io/github/gh-aw-mcpg";

/// 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 @@ -604,7 +588,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 @@ -614,17 +598,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 @@ -21,7 +21,7 @@ use super::common::{
generate_checkout_self, generate_checkout_steps, generate_ci_trigger,
generate_pipeline_path, generate_pipeline_resources, generate_pr_trigger,
generate_repositories, generate_schedule, generate_source_path,
generate_working_directory, replace_with_indent,
generate_working_directory, is_custom_mcp, replace_with_indent,
};
use super::types::{FrontMatter, McpConfig};

Expand Down Expand Up @@ -177,24 +177,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