From 08461fe603bdab13800b2447fe00d94d3a9999b4 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Tue, 10 Mar 2026 08:05:12 -0400 Subject: [PATCH] Add MCP server support to workflow engine and Playwright demo workflow Wire MCP server configs from workflow TOML into agent sessions so MCP tools are available to agent stages during workflow runs. Changes: - run_config.rs: add mcp_servers HashMap to WorkflowRunConfig - backend.rs: AgentApiBackend.with_mcp_servers() passes configs to SessionConfig - run.rs: extract mcp_servers from RunConfig and pass to backend - demo/mod.rs: add default mcp_servers field to struct literals New workflow: arc/workflows/playwright-demo/ - Runs on Daytona with daytona-medium snapshot - Configures Playwright MCP as a sandbox transport (port 3100) - Agent browses Hacker News, takes screenshots at each step - asset_globs captures screenshots/** into logs directory Co-Authored-By: Claude Opus 4.6 --- .../playwright-demo/browse-prompt.md | 14 ++++++++++ arc/workflows/playwright-demo/workflow.dot | 13 +++++++++ arc/workflows/playwright-demo/workflow.toml | 28 +++++++++++++++++++ lib/crates/arc-api/src/demo/mod.rs | 5 ++++ lib/crates/arc-workflows/Cargo.toml | 1 + lib/crates/arc-workflows/src/cli/backend.rs | 12 ++++++++ lib/crates/arc-workflows/src/cli/run.rs | 21 +++++++++++++- .../arc-workflows/src/cli/run_config.rs | 25 +++++++++++++++++ 8 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 arc/workflows/playwright-demo/browse-prompt.md create mode 100644 arc/workflows/playwright-demo/workflow.dot create mode 100644 arc/workflows/playwright-demo/workflow.toml diff --git a/arc/workflows/playwright-demo/browse-prompt.md b/arc/workflows/playwright-demo/browse-prompt.md new file mode 100644 index 000000000..4f63f1311 --- /dev/null +++ b/arc/workflows/playwright-demo/browse-prompt.md @@ -0,0 +1,14 @@ +You have Playwright MCP tools available. Do the following: + +1. First, call the `browser_install` tool to ensure the browser is installed. +2. Create the screenshots directory: `mkdir -p /home/daytona/workspace/screenshots` +3. Use `browser_navigate` to go to https://news.ycombinator.com +4. Use `browser_snapshot` to capture the page content +5. Use `browser_take_screenshot` to save a screenshot to `/home/daytona/workspace/screenshots/01-hn-front-page.png` +6. Click on the first story link +7. Use `browser_take_screenshot` to save a screenshot to `/home/daytona/workspace/screenshots/02-first-story.png` +8. Use `browser_navigate_back` to go back to the front page +9. Click on the "new" link in the nav bar +10. Use `browser_take_screenshot` to save a screenshot to `/home/daytona/workspace/screenshots/03-newest.png` + +After capturing screenshots, write a brief summary of what you found on Hacker News today. diff --git a/arc/workflows/playwright-demo/workflow.dot b/arc/workflows/playwright-demo/workflow.dot new file mode 100644 index 000000000..0908162fa --- /dev/null +++ b/arc/workflows/playwright-demo/workflow.dot @@ -0,0 +1,13 @@ +digraph PlaywrightDemo { + graph [goal="Use Playwright MCP to browse Hacker News and take screenshots", model_stylesheet="* { llm_model: claude-sonnet-4-6; llm_provider: anthropic }"] + rankdir=LR + + start [shape=Mdiamond, label="Start"] + exit [shape=Msquare, label="Exit"] + + install_browser [label="Install Browser", shape=parallelogram, script="npx playwright install --with-deps chromium 2>&1 || true"] + + browse [label="Browse Hacker News", prompt="@browse-prompt.md"] + + start -> install_browser -> browse -> exit +} diff --git a/arc/workflows/playwright-demo/workflow.toml b/arc/workflows/playwright-demo/workflow.toml new file mode 100644 index 000000000..e78ff8309 --- /dev/null +++ b/arc/workflows/playwright-demo/workflow.toml @@ -0,0 +1,28 @@ +version = 1 +goal = "Browse Hacker News with Playwright MCP and capture screenshots" +graph = "workflow.dot" + +[llm] +model = "claude-sonnet-4-6" + +[sandbox] +provider = "daytona" + +[sandbox.daytona] +auto_stop_interval = 30 + +[sandbox.daytona.labels] +project = "playwright-demo" + +[sandbox.daytona.snapshot] +name = "daytona-medium" + +[assets] +include = ["screenshots/**"] + +[mcp_servers.playwright] +type = "sandbox" +command = ["npx", "@playwright/mcp@latest", "--port", "3100", "--headless", "--browser", "chromium"] +port = 3100 +startup_timeout_secs = 60 +tool_timeout_secs = 120 diff --git a/lib/crates/arc-api/src/demo/mod.rs b/lib/crates/arc-api/src/demo/mod.rs index a682e1c7e..b96aebc92 100644 --- a/lib/crates/arc-api/src/demo/mod.rs +++ b/lib/crates/arc-api/src/demo/mod.rs @@ -1295,6 +1295,7 @@ mod runs { checkpoint: Default::default(), pull_request: None, assets: None, + mcp_servers: Default::default(), }) .unwrap() } @@ -1447,6 +1448,7 @@ mod workflows { checkpoint: Default::default(), pull_request: None, assets: None, + mcp_servers: Default::default(), }), graph: r#"digraph fix_build { graph [ @@ -1516,6 +1518,7 @@ mod workflows { checkpoint: Default::default(), pull_request: None, assets: None, + mcp_servers: Default::default(), }), graph: r#"digraph implement { graph [ @@ -1597,6 +1600,7 @@ mod workflows { checkpoint: Default::default(), pull_request: None, assets: None, + mcp_servers: Default::default(), }), graph: r#"digraph sync { graph [ @@ -1667,6 +1671,7 @@ mod workflows { checkpoint: Default::default(), pull_request: None, assets: None, + mcp_servers: Default::default(), }), graph: r#"digraph expand { graph [ diff --git a/lib/crates/arc-workflows/Cargo.toml b/lib/crates/arc-workflows/Cargo.toml index d3daae560..614f6a571 100644 --- a/lib/crates/arc-workflows/Cargo.toml +++ b/lib/crates/arc-workflows/Cargo.toml @@ -22,6 +22,7 @@ anyhow.workspace = true dotenvy.workspace = true arc-agent = { path = "../arc-agent" } arc-exe = { path = "../arc-exe", optional = true } +arc-mcp = { path = "../arc-mcp" } arc-github = { path = "../arc-github" } arc-util = { path = "../arc-util" } arc-git-storage = { path = "../arc-git-storage" } diff --git a/lib/crates/arc-workflows/src/cli/backend.rs b/lib/crates/arc-workflows/src/cli/backend.rs index 8ae83871f..e6bf8b6ff 100644 --- a/lib/crates/arc-workflows/src/cli/backend.rs +++ b/lib/crates/arc-workflows/src/cli/backend.rs @@ -109,6 +109,7 @@ pub struct AgentApiBackend { fallback_chain: Vec, sessions: Mutex>, env: HashMap, + mcp_servers: Vec, } impl AgentApiBackend { @@ -120,6 +121,7 @@ impl AgentApiBackend { fallback_chain, sessions: Mutex::new(HashMap::new()), env: HashMap::new(), + mcp_servers: Vec::new(), } } @@ -129,6 +131,12 @@ impl AgentApiBackend { self } + #[must_use] + pub fn with_mcp_servers(mut self, servers: Vec) -> Self { + self.mcp_servers = servers; + self + } + async fn create_session( &self, node: &Node, @@ -142,6 +150,7 @@ impl AgentApiBackend { sandbox, &self.env, tool_hooks, + self.mcp_servers.clone(), ) .await } @@ -153,6 +162,7 @@ impl AgentApiBackend { sandbox: &Arc, env: &HashMap, tool_hooks: Option>, + mcp_servers: Vec, ) -> Result { let client = Client::from_env() .await @@ -164,6 +174,7 @@ impl AgentApiBackend { max_tokens: node.max_tokens(), reasoning_effort: Some(node.reasoning_effort().to_string()), tool_hooks, + mcp_servers, ..SessionConfig::default() }; @@ -481,6 +492,7 @@ impl CodergenBackend for AgentApiBackend { sandbox, &self.env, tool_hooks.clone(), + self.mcp_servers.clone(), ) .await { diff --git a/lib/crates/arc-workflows/src/cli/run.rs b/lib/crates/arc-workflows/src/cli/run.rs index fc4d4c25a..6b731ff92 100644 --- a/lib/crates/arc-workflows/src/cli/run.rs +++ b/lib/crates/arc-workflows/src/cli/run.rs @@ -893,16 +893,28 @@ pub async fn run_command( .and_then(|c| c.sandbox.as_ref()) .and_then(|s| s.env.clone()) .unwrap_or_default(); + let mcp_servers: Vec = run_cfg + .as_ref() + .map(|c| { + c.mcp_servers + .clone() + .into_iter() + .map(|(name, entry)| entry.into_config(name)) + .collect() + }) + .unwrap_or_default(); let registry = default_registry(interviewer.clone(), { let sandbox_env = sandbox_env.clone(); let model = model.clone(); + let mcp_servers = mcp_servers.clone(); move || { if dry_run_mode { None } else { let api = AgentApiBackend::new(model.clone(), provider_enum, fallback_chain.clone()) - .with_env(sandbox_env.clone()); + .with_env(sandbox_env.clone()) + .with_mcp_servers(mcp_servers.clone()); let cli = AgentCliBackend::new(model.clone(), provider_enum) .with_env(sandbox_env.clone()); Some(Box::new(BackendRouter::new(Box::new(api), cli))) @@ -2119,6 +2131,7 @@ mod tests { checkpoint: Default::default(), pull_request: None, assets: None, + mcp_servers: Default::default(), }; let (model, provider) = resolve_model_provider( Some("gpt-5.2"), @@ -2162,6 +2175,7 @@ mod tests { checkpoint: Default::default(), pull_request: None, assets: None, + mcp_servers: Default::default(), }; let (model, provider) = resolve_model_provider(None, None, Some(&cfg), &defaults, &graph); assert_eq!(model, "toml-model"); @@ -2240,6 +2254,7 @@ mod tests { checkpoint: Default::default(), pull_request: None, assets: None, + mcp_servers: Default::default(), }; let (model, provider) = resolve_model_provider(None, None, Some(&cfg), &defaults, &graph); assert_eq!(model, "toml-model"); @@ -2268,6 +2283,7 @@ mod tests { checkpoint: Default::default(), pull_request: None, assets: None, + mcp_servers: Default::default(), }; let defaults = RunDefaults::default(); assert!(resolve_preserve_sandbox(true, Some(&cfg), &defaults)); @@ -2295,6 +2311,7 @@ mod tests { checkpoint: Default::default(), pull_request: None, assets: None, + mcp_servers: Default::default(), }; let defaults = RunDefaults { sandbox: Some(run_config::SandboxConfig { @@ -2365,6 +2382,7 @@ mod tests { checkpoint: Default::default(), pull_request: None, assets: None, + mcp_servers: Default::default(), }; let defaults = RunDefaults::default(); assert_eq!( @@ -2418,6 +2436,7 @@ mod tests { checkpoint: Default::default(), pull_request: None, assets: None, + mcp_servers: Default::default(), }; let defaults = RunDefaults { sandbox: Some(run_config::SandboxConfig { diff --git a/lib/crates/arc-workflows/src/cli/run_config.rs b/lib/crates/arc-workflows/src/cli/run_config.rs index 756b9e5a7..808762e7e 100644 --- a/lib/crates/arc-workflows/src/cli/run_config.rs +++ b/lib/crates/arc-workflows/src/cli/run_config.rs @@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize}; use tracing::debug; +use arc_mcp::config::{McpServerConfig, McpTransport}; + use crate::daytona_sandbox::{DaytonaConfig, DockerfileSource}; const SUPPORTED_VERSION: u32 = 1; @@ -51,6 +53,29 @@ pub struct WorkflowRunConfig { pub checkpoint: CheckpointConfig, pub pull_request: Option, pub assets: Option, + #[serde(default)] + pub mcp_servers: HashMap, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct McpServerEntry { + #[serde(flatten)] + pub transport: McpTransport, + #[serde(default = "arc_mcp::config::default_startup_timeout_secs")] + pub startup_timeout_secs: u64, + #[serde(default = "arc_mcp::config::default_tool_timeout_secs")] + pub tool_timeout_secs: u64, +} + +impl McpServerEntry { + pub fn into_config(self, name: String) -> McpServerConfig { + McpServerConfig { + name, + transport: self.transport, + startup_timeout_secs: self.startup_timeout_secs, + tool_timeout_secs: self.tool_timeout_secs, + } + } } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]