mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
d426eebf1a
commit
08461fe603
8 changed files with 118 additions and 1 deletions
14
arc/workflows/playwright-demo/browse-prompt.md
Normal file
14
arc/workflows/playwright-demo/browse-prompt.md
Normal file
|
|
@ -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.
|
||||
13
arc/workflows/playwright-demo/workflow.dot
Normal file
13
arc/workflows/playwright-demo/workflow.dot
Normal file
|
|
@ -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
|
||||
}
|
||||
28
arc/workflows/playwright-demo/workflow.toml
Normal file
28
arc/workflows/playwright-demo/workflow.toml
Normal file
|
|
@ -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
|
||||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ pub struct AgentApiBackend {
|
|||
fallback_chain: Vec<FallbackTarget>,
|
||||
sessions: Mutex<HashMap<String, Session>>,
|
||||
env: HashMap<String, String>,
|
||||
mcp_servers: Vec<arc_mcp::config::McpServerConfig>,
|
||||
}
|
||||
|
||||
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<arc_mcp::config::McpServerConfig>) -> 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<dyn Sandbox>,
|
||||
env: &HashMap<String, String>,
|
||||
tool_hooks: Option<Arc<dyn arc_agent::ToolHookCallback>>,
|
||||
mcp_servers: Vec<arc_mcp::config::McpServerConfig>,
|
||||
) -> Result<Session, ArcError> {
|
||||
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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<arc_mcp::config::McpServerConfig> = 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 {
|
||||
|
|
|
|||
|
|
@ -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<PullRequestConfig>,
|
||||
pub assets: Option<AssetsConfig>,
|
||||
#[serde(default)]
|
||||
pub mcp_servers: HashMap<String, McpServerEntry>,
|
||||
}
|
||||
|
||||
#[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)]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue