Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 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
cd85b14
Add tests for mcpg
jamesadevine Apr 11, 2026
5fa9f51
Merge main
jamesadevine Apr 11, 2026
fd2a0e9
chore: merge main into devinejames/remove-mcp-wrapper
jamesadevine Apr 11, 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
220 changes: 100 additions & 120 deletions AGENTS.md

Large diffs are not rendered by default.

377 changes: 375 additions & 2 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ 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"] }
reqwest = { version = "0.12", features = ["json", "blocking"] }
tempfile = "3"
tokio = { version = "1.43", features = ["full"] }
log = "0.4"
Expand All @@ -24,3 +28,5 @@ regex-lite = "0.1"
inquire = { version = "0.9.2", features = ["editor"] }
terminal_size = "0.4.3"
url = "2.5.8"
axum = { version = "0.8.8", features = ["tokio"] }
subtle = "2.6.1"
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
27 changes: 14 additions & 13 deletions src/compile/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
use anyhow::{Context, Result};

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

/// Check if an MCP name is a built-in (known to the Copilot CLI via mcp-metadata.json)
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,10 +304,6 @@ 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()));
Expand Down Expand Up @@ -340,10 +337,6 @@ pub fn generate_copilot_params(front_matter: &FrontMatter) -> String {
}
}

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

params.join(" ")
}

Expand Down Expand Up @@ -506,6 +499,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
69 changes: 52 additions & 17 deletions src/compile/onees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ use std::path::Path;

use super::Compiler;
use super::common::{
self, AWF_VERSION, COPILOT_CLI_VERSION, DEFAULT_POOL, compute_effective_workspace, generate_copilot_params,
self, AWF_VERSION, COPILOT_CLI_VERSION, DEFAULT_POOL, compute_effective_workspace,
generate_acquire_ado_token, generate_checkout_self, generate_checkout_steps,
generate_ci_trigger, generate_copilot_ado_env, generate_executor_ado_env,
generate_header_comment, generate_job_timeout, 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, validate_update_work_item_target,
validate_write_permissions, validate_submit_pr_review_events,
validate_update_pr_votes, validate_resolve_pr_thread_statuses,
generate_ci_trigger, generate_copilot_ado_env, generate_copilot_params,
generate_executor_ado_env, generate_header_comment, generate_job_timeout,
generate_pipeline_path, generate_pipeline_resources, generate_pr_trigger,
generate_repositories, generate_schedule, generate_source_path, generate_working_directory,
is_custom_mcp, replace_with_indent, validate_comment_target,
validate_resolve_pr_thread_statuses, validate_submit_pr_review_events,
validate_update_pr_votes, validate_update_work_item_target, validate_write_permissions,
};
use super::types::{FrontMatter, McpConfig};

Expand Down Expand Up @@ -120,18 +120,30 @@ displayName: "Finalize""#,

// Generate service connection token acquisition steps and env vars
let acquire_read_token = generate_acquire_ado_token(
front_matter.permissions.as_ref().and_then(|p| p.read.as_deref()),
front_matter
.permissions
.as_ref()
.and_then(|p| p.read.as_deref()),
"SC_READ_TOKEN",
);
let copilot_ado_env = generate_copilot_ado_env(
front_matter.permissions.as_ref().and_then(|p| p.read.as_deref()),
front_matter
.permissions
.as_ref()
.and_then(|p| p.read.as_deref()),
);
let acquire_write_token = generate_acquire_ado_token(
front_matter.permissions.as_ref().and_then(|p| p.write.as_deref()),
front_matter
.permissions
.as_ref()
.and_then(|p| p.write.as_deref()),
"SC_WRITE_TOKEN",
);
let executor_ado_env = generate_executor_ado_env(
front_matter.permissions.as_ref().and_then(|p| p.write.as_deref()),
front_matter
.permissions
.as_ref()
.and_then(|p| p.write.as_deref()),
);

// Validate that write-requiring safe-outputs have a write service connection
Expand Down Expand Up @@ -222,24 +234,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