Rename host MCP to workspace MCP for clarity

- Rename binary from host-mcp to workspace-mcp
- Rename src/bin/host_mcp.rs to src/bin/workspace_mcp.rs
- Update tool prefix from host_* to workspace_*
- Update MCP registration name from "host" to "workspace"
- Add "Builtin" tag to UI for workspace and desktop MCPs
- Update documentation (CLAUDE.md, INSTALL.md, docs-site)

The "workspace MCP" name better reflects that it runs in the
workspace's execution context - inside containers for container
workspaces, on host for host workspaces.
This commit is contained in:
Thomas Marchand
2026-01-17 08:56:09 +00:00
parent fa40ad8574
commit cb36560526
11 changed files with 119 additions and 37 deletions

View File

@@ -22,6 +22,21 @@ Open Agent is a managed control plane for OpenCode-based agents. The backend **d
MCPs can be global because and run as child processes on the host or workspace (run inside the container). It depends on the kind of MCP.
## Container Execution Model (Important!)
When a **mission runs in a container workspace**, bash commands execute **inside the container**, not on the host. Here's why:
1. OpenCode's built-in Bash tool is **disabled** for container workspaces
2. Agents use `workspace_bash` from the "workspace MCP" instead
3. The "workspace MCP" command is **wrapped in systemd-nspawn** at startup
4. Therefore all `workspace_bash` commands run inside the container with container networking
The "workspace MCP" is named this way because it runs in the **workspace's execution context** - for container workspaces, that means inside the container.
See `src/workspace.rs` lines 590-640 (nspawn wrapping) and 714-720 (tool configuration).
**Contrast with workspace exec API**: The `/api/workspaces/:id/exec` endpoint also runs commands inside containers (via nspawn), but is subject to HTTP timeouts. The mission system uses SSE streaming with no timeout.
## Design Guardrails
- Do **not** reintroduce autonomous agent logic (budgeting, task splitting, verification, model selection). OpenCode handles execution.

View File

@@ -79,8 +79,8 @@ name = "desktop-mcp"
path = "src/bin/desktop_mcp.rs"
[[bin]]
name = "host-mcp"
path = "src/bin/host_mcp.rs"
name = "workspace-mcp"
path = "src/bin/workspace_mcp.rs"
[dev-dependencies]
tokio-test = "0.4"

View File

@@ -349,15 +349,15 @@ cd /opt/open_agent/vaduz-v1
source /root/.cargo/env
# Debug build (fast) - recommended for rapid iteration
cargo build --bin open_agent --bin host-mcp --bin desktop-mcp
cargo build --bin open_agent --bin workspace-mcp --bin desktop-mcp
install -m 0755 target/debug/open_agent /usr/local/bin/open_agent
install -m 0755 target/debug/host-mcp /usr/local/bin/host-mcp
install -m 0755 target/debug/workspace-mcp /usr/local/bin/workspace-mcp
install -m 0755 target/debug/desktop-mcp /usr/local/bin/desktop-mcp
# Or: Release build (slower compile, faster runtime)
# cargo build --release --bin open_agent --bin host-mcp --bin desktop-mcp
# cargo build --release --bin open_agent --bin workspace-mcp --bin desktop-mcp
# install -m 0755 target/release/open_agent /usr/local/bin/open_agent
# install -m 0755 target/release/host-mcp /usr/local/bin/host-mcp
# install -m 0755 target/release/workspace-mcp /usr/local/bin/workspace-mcp
# install -m 0755 target/release/desktop-mcp /usr/local/bin/desktop-mcp
```
@@ -611,9 +611,9 @@ cd /opt/open_agent/vaduz-v1
git fetch --tags origin
git checkout <version-tag> # e.g., v0.2.1
source /root/.cargo/env
cargo build --bin open_agent --bin host-mcp --bin desktop-mcp
cargo build --bin open_agent --bin workspace-mcp --bin desktop-mcp
install -m 0755 target/debug/open_agent /usr/local/bin/open_agent
install -m 0755 target/debug/host-mcp /usr/local/bin/host-mcp
install -m 0755 target/debug/workspace-mcp /usr/local/bin/workspace-mcp
install -m 0755 target/debug/desktop-mcp /usr/local/bin/desktop-mcp
systemctl restart open_agent.service
```

View File

@@ -301,6 +301,9 @@ function RuntimeMcpCard({
<div className="flex items-center gap-2">
<h3 className="font-medium text-white truncate">{mcp.name}</h3>
<span className="tag bg-cyan-500/10 text-cyan-400 border-cyan-500/20">Runtime</span>
{(mcp.name === 'workspace' || mcp.name === 'desktop') && (
<span className="tag bg-violet-500/10 text-violet-400 border-violet-500/20">Builtin</span>
)}
<span
className={cn(
'tag',
@@ -527,6 +530,9 @@ function RuntimeMcpDetailPanel({
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold text-white">{mcp.name}</h2>
<span className="tag bg-cyan-500/10 text-cyan-400 border-cyan-500/20">Runtime</span>
{(mcp.name === 'workspace' || mcp.name === 'desktop') && (
<span className="tag bg-violet-500/10 text-violet-400 border-violet-500/20">Builtin</span>
)}
<span
className={cn(
'tag',

View File

@@ -68,6 +68,16 @@ nav.nextra-nav-container {
main.nextra-content {
background-color: rgb(var(--background));
border-radius: 0 !important;
}
/* Ensure no rounded corners on any layout containers */
main.nextra-content,
.nextra-main-content,
[class*="main-content"],
article {
border-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
.nextra-toc {
@@ -824,6 +834,54 @@ aside a:hover,
background-color: rgba(255, 248, 240, 0.04);
}
/* ============================================
SIDEBAR BOTTOM-LEFT CORNER FIX
============================================ */
/* Fix the rounded corner artifact in the bottom-left of the sidebar */
.nextra-sidebar-container,
aside.nextra-sidebar-container,
[class*="sidebar-container"],
aside[class*="nextra"] {
border-radius: 0 !important;
}
/* Target the theme switcher container at the bottom of the sidebar */
.nextra-sidebar-container > div:last-child,
aside > div:last-child,
aside > div:last-of-type {
border-radius: 0 !important;
}
/* Ensure the sidebar footer area has no rounded corners */
[class*="sidebar"] [class*="footer"],
[class*="sidebar"] > :last-child,
aside [class*="theme"],
aside [class*="toggle"] {
border-radius: 0 !important;
}
/* Target any nested containers that might have rounded corners */
.nextra-sidebar-container *,
aside.nextra-sidebar-container * {
border-bottom-left-radius: 0 !important;
}
/* Specifically remove the bottom-left radius from the sidebar wrapper */
.nextra-sidebar-container > div,
aside > div {
border-bottom-left-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
/* Fix any pseudo-elements that might create the corner effect */
.nextra-sidebar-container::after,
.nextra-sidebar-container::before,
aside::after,
aside::before {
border-radius: 0 !important;
}
/* ============================================
MOBILE SIDEBAR FIXES
============================================ */

View File

@@ -64,9 +64,9 @@ cd /opt/open_agent
git clone https://github.com/Th0rgal/open-agent vaduz-v1
cd vaduz-v1
cargo build --bin open_agent --bin host-mcp --bin desktop-mcp
cargo build --bin open_agent --bin workspace-mcp --bin desktop-mcp
install -m 0755 target/debug/open_agent /usr/local/bin/open_agent
install -m 0755 target/debug/host-mcp /usr/local/bin/host-mcp
install -m 0755 target/debug/workspace-mcp /usr/local/bin/workspace-mcp
install -m 0755 target/debug/desktop-mcp /usr/local/bin/desktop-mcp
```

View File

@@ -576,7 +576,7 @@ fn stream_open_agent_update() -> impl Stream<Item = Result<Event, std::convert::
// Source cargo env and build
let build_result = Command::new("bash")
.args(["-c", "source /root/.cargo/env && cargo build --bin open_agent --bin host-mcp --bin desktop-mcp"])
.args(["-c", "source /root/.cargo/env && cargo build --bin open_agent --bin workspace-mcp --bin desktop-mcp"])
.current_dir(OPEN_AGENT_REPO_PATH)
.output()
.await;
@@ -620,7 +620,7 @@ fn stream_open_agent_update() -> impl Stream<Item = Result<Event, std::convert::
let binaries = [
("open_agent", "/usr/local/bin/open_agent"),
("host-mcp", "/usr/local/bin/host-mcp"),
("workspace-mcp", "/usr/local/bin/workspace-mcp"),
("desktop-mcp", "/usr/local/bin/desktop-mcp"),
];

View File

@@ -366,11 +366,11 @@ fn debug_log(tag: &str, payload: &Value) {
if std::env::var("OPEN_AGENT_MCP_DEBUG").ok().as_deref() != Some("1") {
return;
}
let line = format!("[host-mcp] {} {}\n", tag, payload);
let line = format!("[workspace-mcp] {} {}\n", tag, payload);
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open("/tmp/host-mcp-debug.log")
.open("/tmp/workspace-mcp-debug.log")
{
let _ = file.write_all(line.as_bytes());
}
@@ -596,7 +596,7 @@ fn handle_request(
json!({
"protocolVersion": "2024-11-05",
"serverInfo": {
"name": "host-mcp",
"name": "workspace-mcp",
"version": env!("CARGO_PKG_VERSION"),
},
"capabilities": {
@@ -644,7 +644,7 @@ fn handle_request(
}
fn main() {
eprintln!("[host-mcp] Starting MCP server for host tools...");
eprintln!("[workspace-mcp] Starting MCP server for workspace tools...");
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()

View File

@@ -158,24 +158,24 @@ impl McpRegistry {
);
desktop.scope = McpScope::Workspace;
let host_command = {
let release = working_dir.join("target").join("release").join("host-mcp");
let debug = working_dir.join("target").join("debug").join("host-mcp");
let workspace_command = {
let release = working_dir.join("target").join("release").join("workspace-mcp");
let debug = working_dir.join("target").join("debug").join("workspace-mcp");
if release.exists() {
release.to_string_lossy().to_string()
} else if debug.exists() {
debug.to_string_lossy().to_string()
} else {
"host-mcp".to_string()
"workspace-mcp".to_string()
}
};
let mut host = McpServerConfig::new_stdio(
"host".to_string(),
host_command,
let mut workspace = McpServerConfig::new_stdio(
"workspace".to_string(),
workspace_command,
Vec::new(),
HashMap::new(),
);
host.scope = McpScope::Workspace;
workspace.scope = McpScope::Workspace;
// Prefer bunx (Bun) when present, but fall back to npx for compatibility.
let js_runner = if command_exists("bunx") {
"bunx"
@@ -194,7 +194,7 @@ impl McpRegistry {
);
playwright.scope = McpScope::Workspace;
vec![host, desktop, playwright]
vec![workspace, desktop, playwright]
}
async fn ensure_defaults(
@@ -274,11 +274,11 @@ impl McpRegistry {
.await;
}
// Prefer repo-local MCP binaries for host/desktop (debug or release),
// Prefer repo-local MCP binaries for workspace/desktop (debug or release),
// so default configs work without installing to PATH.
for config in configs.iter_mut() {
let binary_name = match config.name.as_str() {
"host" => Some("host-mcp"),
"workspace" => Some("workspace-mcp"),
"desktop" => Some("desktop-mcp"),
_ => None,
};

View File

@@ -351,7 +351,7 @@ pub async fn ensure_global_config(mcp: &McpRegistry) -> anyhow::Result<()> {
tools_obj.insert("desktop_*".to_string(), json!(true));
tools_obj.insert("playwright_*".to_string(), json!(true));
tools_obj.insert("browser_*".to_string(), json!(true));
tools_obj.insert("host_*".to_string(), json!(true));
tools_obj.insert("workspace_*".to_string(), json!(true));
let payload = serde_json::to_string_pretty(&root)?;
tokio::fs::write(&config_path, payload).await?;

View File

@@ -711,19 +711,22 @@ async fn write_opencode_config(
}
}
// Disable OpenCode's builtin bash tools so agents must use the host MCP's bash.
// For container (nspawn) workspaces, the host MCP runs INSIDE the container via
// systemd-nspawn wrapping, so its bash tool has container networking (Tailscale, etc).
// Disable OpenCode's builtin bash tools so agents must use the workspace MCP's bash.
//
// The "workspace MCP" is the MCP provided by Open Agent that runs in the workspace's
// execution context. For container (nspawn) workspaces, this MCP runs INSIDE the
// container via systemd-nspawn wrapping (see lines 590-640), so its bash tool executes
// commands inside the container with container networking (Tailscale, etc).
// For host workspaces, disable bash entirely (security: no host shell access).
let mut tools = serde_json::Map::new();
match workspace_type {
WorkspaceType::Chroot => {
// Disable OpenCode built-in bash - agents must use host MCP's bash
// Disable OpenCode built-in bash - agents must use workspace MCP's bash
// which runs inside the container with container networking
tools.insert("Bash".to_string(), json!(false)); // Claude Code built-in
tools.insert("bash".to_string(), json!(false)); // lowercase variant
// Enable MCP-provided tools (host MCP runs inside container via nspawn)
tools.insert("host_*".to_string(), json!(true));
// Enable MCP-provided tools (workspace MCP runs inside container via nspawn)
tools.insert("workspace_*".to_string(), json!(true));
tools.insert("desktop_*".to_string(), json!(true));
tools.insert("playwright_*".to_string(), json!(true));
tools.insert("browser_*".to_string(), json!(true));
@@ -735,8 +738,8 @@ async fn write_opencode_config(
tools.insert("desktop_*".to_string(), json!(false));
tools.insert("playwright_*".to_string(), json!(false));
tools.insert("browser_*".to_string(), json!(false));
// Only allow host MCP tools (files, etc)
tools.insert("host_*".to_string(), json!(true));
// Only allow workspace MCP tools (files, etc)
tools.insert("workspace_*".to_string(), json!(true));
}
}
config_json.insert("tools".to_string(), serde_json::Value::Object(tools));
@@ -1648,7 +1651,7 @@ async fn sync_workspace_mcp_binaries(
working_dir: &Path,
container_root: &Path,
) -> anyhow::Result<()> {
for binary in ["host-mcp", "desktop-mcp"] {
for binary in ["workspace-mcp", "desktop-mcp"] {
copy_binary_into_container(working_dir, container_root, binary).await?;
}
Ok(())