From f51c9fbade151baeb9f1dbaf8322c8752af21c0b Mon Sep 17 00:00:00 2001 From: kanywst Date: Sun, 24 May 2026 20:14:56 +0900 Subject: [PATCH 1/6] feat(cli/mcp): add 'zeus mcp' subcommand scaffold Registers 'code mcp' (Zeus's 'zeus mcp') as a clap subcommand that takes --transport (stdio|sse), --port, and --workspace flags. The handler is a stub that prints which transport was selected; real implementation lands in a follow-up PR. 'cargo check' and 'cargo build --bin code' pass locally. Smoke tested: $ ./target/debug/code mcp --help $ ./target/debug/code mcp zeus mcp: stdio transport not yet implemented (workspace=...) Design doc: docs/zeus-mcp-server.md --- cli/src/bin/code/main.rs | 6 ++-- cli/src/commands.rs | 1 + cli/src/commands/args.rs | 28 +++++++++++++++++ cli/src/commands/mcp.rs | 35 +++++++++++++++++++++ docs/zeus-mcp-server.md | 66 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 cli/src/commands/mcp.rs create mode 100644 docs/zeus-mcp-server.md diff --git a/cli/src/bin/code/main.rs b/cli/src/bin/code/main.rs index 8a9c38f3..32644cd0 100644 --- a/cli/src/bin/code/main.rs +++ b/cli/src/bin/code/main.rs @@ -9,8 +9,8 @@ use std::process::Command; use clap::Parser; use cli::{ commands::{ - agent_host, agent_kill, agent_logs, agent_ps, agent_stop, args, serve_web, tunnels, update, - version, CommandContext, + agent_host, agent_kill, agent_logs, agent_ps, agent_stop, args, mcp, serve_web, tunnels, + update, version, CommandContext, }, constants::get_default_user_agent, desktop, log, @@ -104,6 +104,8 @@ async fn main() -> Result<(), std::convert::Infallible> { serve_web::serve_web(context!(), sw_args).await } + Some(args::Commands::Mcp(mcp_args)) => mcp::mcp(mcp_args).await, + Some(args::Commands::Agent(agent_args)) => match agent_args.subcommand { Some(args::AgentSubcommand::Ps(ps_args)) => { agent_ps::agent_ps(context!(), ps_args).await diff --git a/cli/src/commands.rs b/cli/src/commands.rs index eeb8fc53..d502a8eb 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -12,6 +12,7 @@ pub mod agent_logs; pub mod agent_ps; pub mod agent_stop; pub mod args; +pub mod mcp; pub mod output; pub mod serve_web; pub mod tunnels; diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index ef6daa07..9e11e2fe 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -189,6 +189,34 @@ pub enum Commands { /// Manage agent host sessions. #[clap(name = "agent")] Agent(AgentArgs), + + /// Expose the editor as a Model Context Protocol (MCP) server. + #[clap(name = "mcp")] + Mcp(McpArgs), +} + +#[derive(Args, Debug, Clone)] +pub struct McpArgs { + /// Transport to use. `stdio` for line-oriented clients (Claude Code CLI + /// and similar), `sse` for HTTP clients. + #[clap(long, default_value = "stdio")] + pub transport: McpTransport, + + /// Port to bind to when --transport=sse. 0 (default) picks an + /// ephemeral port; the chosen port is printed on startup. + #[clap(long, default_value_t = 0)] + pub port: u16, + + /// Workspace root. Defaults to the current working directory. The + /// server refuses operations on paths outside this root. + #[clap(long)] + pub workspace: Option, +} + +#[derive(ValueEnum, Debug, Clone, Copy)] +pub enum McpTransport { + Stdio, + Sse, } #[derive(Args, Debug, Clone)] diff --git a/cli/src/commands/mcp.rs b/cli/src/commands/mcp.rs new file mode 100644 index 00000000..0d2faf6f --- /dev/null +++ b/cli/src/commands/mcp.rs @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Zeus — Model Context Protocol (MCP) server entry point. +// Scaffold only; real implementation lands in a follow-up PR. See +// docs/zeus-mcp-server.md for the tool surface and design. + +use crate::commands::args::{McpArgs, McpTransport}; +use crate::util::errors::AnyError; + +pub async fn mcp(args: McpArgs) -> Result { + let workspace = args + .workspace + .unwrap_or_else(|| std::env::current_dir().expect("cwd is readable")); + + match args.transport { + McpTransport::Stdio => { + eprintln!( + "zeus mcp: stdio transport not yet implemented (workspace={})", + workspace.display() + ); + } + McpTransport::Sse => { + eprintln!( + "zeus mcp: sse transport not yet implemented (port={}, workspace={})", + args.port, + workspace.display() + ); + } + } + + Ok(0) +} diff --git a/docs/zeus-mcp-server.md b/docs/zeus-mcp-server.md new file mode 100644 index 00000000..000cffe1 --- /dev/null +++ b/docs/zeus-mcp-server.md @@ -0,0 +1,66 @@ +# `zeus mcp` — Zeus as an MCP server + +Zeus exposes its editor surface as an MCP server. Any MCP-aware client (Claude Code CLI, ChatGPT desktop, another editor, a script) can read buffers, apply edits, run commands, and start subagents through one protocol. + +Implementation lives in two places: + +- `cli/src/commands/mcp.rs` (this PR): the entry point — `code mcp` / `zeus mcp` subcommand +- `src/vs/workbench/contrib/mcpServer/` (later PR): the actual tool implementations, which talk to the workbench + +## Subcommand surface + +```text +zeus mcp [--transport stdio|sse] [--port N] [--workspace PATH] +``` + +- `--transport`: defaults to `stdio` (Anthropic / Claude Code CLI standard). `sse` for HTTP clients. +- `--port`: only meaningful with `--transport sse`. Defaults to a random ephemeral port; the chosen port is printed on stdout. +- `--workspace`: workspace root path. Defaults to `$PWD`. + +The Rust CLI launches a headless workbench process (or attaches to a running one if available) and proxies MCP traffic. + +## Initial tool surface + +```text +buffer_get(path) -> { content, language } +buffer_set(path, content) -> { ok } +edit_apply(path, range, text) -> { ok } +diagnostics_get(path?) -> { diagnostics[] } +selection_get() -> { path, range, text } | null +command_run(name, args?) -> { result } +visible_files() -> { paths[] } +search_workspace(query, opts?) -> { matches[] } +agent_start(skill, prompt) -> { agent_id } +agent_status(agent_id) -> { state, progress? } +agent_cancel(agent_id) -> { ok } +git_diff(staged?) -> { diff } +lsp_definitions(path, pos) -> { locations[] } +lsp_references(path, pos) -> { locations[] } +``` + +Detailed schemas land alongside the implementation PR. + +## Authentication + +MCP over stdio inherits the calling process's permissions; no extra auth. + +MCP over SSE binds to `127.0.0.1` only by default and requires a bearer token printed to stdout on start (`zeus mcp --transport sse` prints `Token: ...` to stderr). Binding to non-loopback requires `--bind 0.0.0.0` and is gated by a confirmation flag. + +## Workspace isolation + +A single Zeus install can host multiple `zeus mcp` instances, one per workspace. Each instance is scoped to its workspace root and refuses operations on paths outside it. + +## Why headless and not just an extension? + +A VS Code extension only runs inside the editor's UI process. We want this to work when no editor is open — for example, a CI job that wants to call `buffer_get` after applying a refactor PR. The CLI gives us that decoupling. + +## Acceptance criteria for the implementation PR (not this scaffold PR) + +- `code mcp` boots, prints transport info, and accepts an MCP `initialize` request +- `buffer_get` and `buffer_set` round-trip a small file in tests +- Refuses paths outside the workspace +- Plays nicely as a subprocess of Claude Code CLI + +## Status + +Scaffold only. The Rust command is registered and prints a stub message; real implementation is a follow-up. From 8f5858577d28e7060d2214fc6b94981486af2d0b Mon Sep 17 00:00:00 2001 From: kanywst Date: Sun, 24 May 2026 21:14:14 +0900 Subject: [PATCH 2/6] fix(cli/mcp): propagate cwd error, stderr-consistent docs - cli/src/commands/mcp.rs: replace expect() on current_dir() with ? via wrap(). Prevents a panic if cwd is inaccessible. - docs/zeus-mcp-server.md: clarify that the chosen SSE port is printed on stderr (was 'stdout'). Stdout is reserved for protocol traffic on stdio transport; stderr is consistent across transports. cargo check passes. --- cli/src/commands/mcp.rs | 9 +++++---- docs/zeus-mcp-server.md | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cli/src/commands/mcp.rs b/cli/src/commands/mcp.rs index 0d2faf6f..e0770edb 100644 --- a/cli/src/commands/mcp.rs +++ b/cli/src/commands/mcp.rs @@ -8,12 +8,13 @@ // docs/zeus-mcp-server.md for the tool surface and design. use crate::commands::args::{McpArgs, McpTransport}; -use crate::util::errors::AnyError; +use crate::util::errors::{wrap, AnyError}; pub async fn mcp(args: McpArgs) -> Result { - let workspace = args - .workspace - .unwrap_or_else(|| std::env::current_dir().expect("cwd is readable")); + let workspace = match args.workspace { + Some(p) => p, + None => std::env::current_dir().map_err(|e| wrap(e, "could not resolve workspace from cwd"))?, + }; match args.transport { McpTransport::Stdio => { diff --git a/docs/zeus-mcp-server.md b/docs/zeus-mcp-server.md index 000cffe1..030dda90 100644 --- a/docs/zeus-mcp-server.md +++ b/docs/zeus-mcp-server.md @@ -14,7 +14,7 @@ zeus mcp [--transport stdio|sse] [--port N] [--workspace PATH] ``` - `--transport`: defaults to `stdio` (Anthropic / Claude Code CLI standard). `sse` for HTTP clients. -- `--port`: only meaningful with `--transport sse`. Defaults to a random ephemeral port; the chosen port is printed on stdout. +- `--port`: only meaningful with `--transport sse`. Defaults to a random ephemeral port; the chosen port is printed on stderr (stdout is reserved for protocol traffic on the stdio transport, and we keep stderr consistent across transports). - `--workspace`: workspace root path. Defaults to `$PWD`. The Rust CLI launches a headless workbench process (or attaches to a running one if available) and proxies MCP traffic. From 72e82682460d12bf86a0520f4e5acc1fc3b36009 Mon Sep 17 00:00:00 2001 From: kanywst Date: Sun, 24 May 2026 23:33:37 +0900 Subject: [PATCH 3/6] fix(cli/mcp): take CommandContext, add --bind / --allow-non-loopback, canonicalize workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mcp() now takes CommandContext like every other command handler. Real implementation will need it for logging and workbench discovery; passing it from the dispatch site now keeps the signature stable. - McpArgs gains '--bind' (defaults to 127.0.0.1) and '--allow-non-loopback' (required when --bind is non-loopback). The docs already promised these — adding them now to keep the CLI surface in lockstep with the design. - Workspace path is canonicalized at entry. The 'refuse paths outside the workspace' security check can then use byte-prefix comparison instead of re-resolving relative segments per request. - docs/zeus-mcp-server.md: fix the stdout/stderr contradiction in the auth section. Bearer token is on stderr (stdout is reserved for protocol traffic on stdio transport). --- cli/src/bin/code/main.rs | 2 +- cli/src/commands/args.rs | 14 +++++++++++++- cli/src/commands/mcp.rs | 14 +++++++++++--- docs/zeus-mcp-server.md | 2 +- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/cli/src/bin/code/main.rs b/cli/src/bin/code/main.rs index 32644cd0..d0a5b470 100644 --- a/cli/src/bin/code/main.rs +++ b/cli/src/bin/code/main.rs @@ -104,7 +104,7 @@ async fn main() -> Result<(), std::convert::Infallible> { serve_web::serve_web(context!(), sw_args).await } - Some(args::Commands::Mcp(mcp_args)) => mcp::mcp(mcp_args).await, + Some(args::Commands::Mcp(mcp_args)) => mcp::mcp(context!(), mcp_args).await, Some(args::Commands::Agent(agent_args)) => match agent_args.subcommand { Some(args::AgentSubcommand::Ps(ps_args)) => { diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 9e11e2fe..bdd3b35f 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -203,10 +203,22 @@ pub struct McpArgs { pub transport: McpTransport, /// Port to bind to when --transport=sse. 0 (default) picks an - /// ephemeral port; the chosen port is printed on startup. + /// ephemeral port; the chosen port is printed on stderr at startup. #[clap(long, default_value_t = 0)] pub port: u16, + /// Interface to bind on when --transport=sse. Defaults to `127.0.0.1` + /// (loopback). Binding to a non-loopback interface (`0.0.0.0`, a LAN + /// IP, etc.) also requires `--allow-non-loopback` to confirm intent. + #[clap(long, default_value = "127.0.0.1")] + pub bind: String, + + /// Acknowledge that binding the SSE transport to a non-loopback + /// interface is intentional. Required whenever `--bind` is set to + /// anything other than `127.0.0.1`/`localhost`/`::1`. + #[clap(long)] + pub allow_non_loopback: bool, + /// Workspace root. Defaults to the current working directory. The /// server refuses operations on paths outside this root. #[clap(long)] diff --git a/cli/src/commands/mcp.rs b/cli/src/commands/mcp.rs index e0770edb..3bc5ee39 100644 --- a/cli/src/commands/mcp.rs +++ b/cli/src/commands/mcp.rs @@ -8,14 +8,21 @@ // docs/zeus-mcp-server.md for the tool surface and design. use crate::commands::args::{McpArgs, McpTransport}; +use crate::commands::CommandContext; use crate::util::errors::{wrap, AnyError}; -pub async fn mcp(args: McpArgs) -> Result { - let workspace = match args.workspace { +pub async fn mcp(_ctx: CommandContext, args: McpArgs) -> Result { + let raw_workspace = match args.workspace { Some(p) => p, None => std::env::current_dir().map_err(|e| wrap(e, "could not resolve workspace from cwd"))?, }; + // Canonicalize so the security check ("refuses operations on paths + // outside this root") can rely on byte-prefix comparison instead of + // having to re-resolve relative segments on every request. + let workspace = std::fs::canonicalize(&raw_workspace) + .map_err(|e| wrap(e, format!("could not canonicalize workspace path {}", raw_workspace.display())))?; + match args.transport { McpTransport::Stdio => { eprintln!( @@ -25,8 +32,9 @@ pub async fn mcp(args: McpArgs) -> Result { } McpTransport::Sse => { eprintln!( - "zeus mcp: sse transport not yet implemented (port={}, workspace={})", + "zeus mcp: sse transport not yet implemented (port={}, bind={}, workspace={})", args.port, + args.bind, workspace.display() ); } diff --git a/docs/zeus-mcp-server.md b/docs/zeus-mcp-server.md index 030dda90..3ceae1d0 100644 --- a/docs/zeus-mcp-server.md +++ b/docs/zeus-mcp-server.md @@ -44,7 +44,7 @@ Detailed schemas land alongside the implementation PR. MCP over stdio inherits the calling process's permissions; no extra auth. -MCP over SSE binds to `127.0.0.1` only by default and requires a bearer token printed to stdout on start (`zeus mcp --transport sse` prints `Token: ...` to stderr). Binding to non-loopback requires `--bind 0.0.0.0` and is gated by a confirmation flag. +MCP over SSE binds to `127.0.0.1` only by default and requires a bearer token printed on **stderr** on start (`Token: ...`). Stdout is reserved for protocol traffic on the stdio transport, and we keep stderr consistent across transports. Binding to non-loopback (`--bind 0.0.0.0`, a LAN IP, etc.) additionally requires `--allow-non-loopback` as a confirmation flag. ## Workspace isolation From e6e8d9402ca60f42296ebf6abb928f942e91b59a Mon Sep 17 00:00:00 2001 From: kanywst Date: Mon, 25 May 2026 00:01:40 +0900 Subject: [PATCH 4/6] feat(cli/mcp): validate workspace is dir, refuse non-loopback bind without flag --- cli/src/commands/args.rs | 2 +- cli/src/commands/mcp.rs | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index bdd3b35f..cbdb3732 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -225,7 +225,7 @@ pub struct McpArgs { pub workspace: Option, } -#[derive(ValueEnum, Debug, Clone, Copy)] +#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)] pub enum McpTransport { Stdio, Sse, diff --git a/cli/src/commands/mcp.rs b/cli/src/commands/mcp.rs index 3bc5ee39..b781b6c0 100644 --- a/cli/src/commands/mcp.rs +++ b/cli/src/commands/mcp.rs @@ -7,9 +7,11 @@ // Scaffold only; real implementation lands in a follow-up PR. See // docs/zeus-mcp-server.md for the tool surface and design. +use std::net::IpAddr; + use crate::commands::args::{McpArgs, McpTransport}; use crate::commands::CommandContext; -use crate::util::errors::{wrap, AnyError}; +use crate::util::errors::{wrap, AnyError, SetupError}; pub async fn mcp(_ctx: CommandContext, args: McpArgs) -> Result { let raw_workspace = match args.workspace { @@ -23,6 +25,14 @@ pub async fn mcp(_ctx: CommandContext, args: McpArgs) -> Result { let workspace = std::fs::canonicalize(&raw_workspace) .map_err(|e| wrap(e, format!("could not canonicalize workspace path {}", raw_workspace.display())))?; + if !workspace.is_dir() { + return Err(SetupError(format!( + "workspace path is not a directory: {}", + workspace.display() + )) + .into()); + } + match args.transport { McpTransport::Stdio => { eprintln!( @@ -31,6 +41,23 @@ pub async fn mcp(_ctx: CommandContext, args: McpArgs) -> Result { ); } McpTransport::Sse => { + // Refuse to bind to a non-loopback interface without explicit opt-in, + // to keep the default posture local-only. See docs/zeus-mcp-server.md. + let bind_addr: IpAddr = args.bind.parse().map_err(|e| { + wrap( + e, + format!("invalid --bind address: {} (expected an IP literal)", args.bind), + ) + })?; + if !bind_addr.is_loopback() && !args.allow_non_loopback { + return Err(SetupError(format!( + "refusing to bind SSE transport on non-loopback address {} \ +without --allow-non-loopback (use 127.0.0.1 / ::1 for local-only)", + args.bind + )) + .into()); + } + eprintln!( "zeus mcp: sse transport not yet implemented (port={}, bind={}, workspace={})", args.port, From aa4125681a4d46beccff3d7fa3fbdc8f04f9fd28 Mon Sep 17 00:00:00 2001 From: kanywst Date: Mon, 25 May 2026 00:24:25 +0900 Subject: [PATCH 5/6] refactor(cli/mcp): use std::net::IpAddr for --bind so clap validates at parse time --- cli/src/commands/args.rs | 10 ++++++---- cli/src/commands/mcp.rs | 14 ++++---------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index cbdb3732..3bb0ae07 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -208,14 +208,16 @@ pub struct McpArgs { pub port: u16, /// Interface to bind on when --transport=sse. Defaults to `127.0.0.1` - /// (loopback). Binding to a non-loopback interface (`0.0.0.0`, a LAN - /// IP, etc.) also requires `--allow-non-loopback` to confirm intent. + /// (loopback). Only IP literals are accepted — pass `127.0.0.1` / + /// `::1` for loopback, not `localhost`. Binding to a non-loopback + /// interface (`0.0.0.0`, a LAN IP, etc.) also requires + /// `--allow-non-loopback` to confirm intent. #[clap(long, default_value = "127.0.0.1")] - pub bind: String, + pub bind: std::net::IpAddr, /// Acknowledge that binding the SSE transport to a non-loopback /// interface is intentional. Required whenever `--bind` is set to - /// anything other than `127.0.0.1`/`localhost`/`::1`. + /// anything other than `127.0.0.1` / `::1`. #[clap(long)] pub allow_non_loopback: bool, diff --git a/cli/src/commands/mcp.rs b/cli/src/commands/mcp.rs index b781b6c0..4090c8e8 100644 --- a/cli/src/commands/mcp.rs +++ b/cli/src/commands/mcp.rs @@ -7,8 +7,6 @@ // Scaffold only; real implementation lands in a follow-up PR. See // docs/zeus-mcp-server.md for the tool surface and design. -use std::net::IpAddr; - use crate::commands::args::{McpArgs, McpTransport}; use crate::commands::CommandContext; use crate::util::errors::{wrap, AnyError, SetupError}; @@ -42,14 +40,10 @@ pub async fn mcp(_ctx: CommandContext, args: McpArgs) -> Result { } McpTransport::Sse => { // Refuse to bind to a non-loopback interface without explicit opt-in, - // to keep the default posture local-only. See docs/zeus-mcp-server.md. - let bind_addr: IpAddr = args.bind.parse().map_err(|e| { - wrap( - e, - format!("invalid --bind address: {} (expected an IP literal)", args.bind), - ) - })?; - if !bind_addr.is_loopback() && !args.allow_non_loopback { + // to keep the default posture local-only. clap already parsed + // `--bind` into `IpAddr`, so no string-level validation needed here. + // See docs/zeus-mcp-server.md. + if !args.bind.is_loopback() && !args.allow_non_loopback { return Err(SetupError(format!( "refusing to bind SSE transport on non-loopback address {} \ without --allow-non-loopback (use 127.0.0.1 / ::1 for local-only)", From ec322b00dedcbd80ba73d5980abbf47cfc7197ac Mon Sep 17 00:00:00 2001 From: kanywst Date: Mon, 25 May 2026 22:50:19 +0900 Subject: [PATCH 6/6] feat(cli/mcp): non-zero exit for unimplemented transport, full synopsis in docs Address remaining reviewer feedback: - Replace Ok(0) returns for unimplemented stdio/sse transports with SetupError so a script that pipes 'zeus mcp' into a real MCP client doesn't appear to succeed silently. The eprintln!() lines were also redundant with the SetupError display, so they're folded in. - Docs synopsis now lists --bind and --allow-non-loopback alongside --transport / --port / --workspace; previously the flags were implemented and documented further down, but the at-a-glance command line was incomplete. --- cli/src/commands/mcp.rs | 15 +++++++++------ docs/zeus-mcp-server.md | 4 +++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/cli/src/commands/mcp.rs b/cli/src/commands/mcp.rs index 4090c8e8..683299a7 100644 --- a/cli/src/commands/mcp.rs +++ b/cli/src/commands/mcp.rs @@ -33,10 +33,14 @@ pub async fn mcp(_ctx: CommandContext, args: McpArgs) -> Result { match args.transport { McpTransport::Stdio => { - eprintln!( + // Scaffold-only: surface this as a SetupError (non-zero exit) + // rather than Ok(0). A silent exit-0 would let scripts that + // pipe this command into a real MCP client appear to succeed. + Err(SetupError(format!( "zeus mcp: stdio transport not yet implemented (workspace={})", workspace.display() - ); + )) + .into()) } McpTransport::Sse => { // Refuse to bind to a non-loopback interface without explicit opt-in, @@ -52,14 +56,13 @@ without --allow-non-loopback (use 127.0.0.1 / ::1 for local-only)", .into()); } - eprintln!( + Err(SetupError(format!( "zeus mcp: sse transport not yet implemented (port={}, bind={}, workspace={})", args.port, args.bind, workspace.display() - ); + )) + .into()) } } - - Ok(0) } diff --git a/docs/zeus-mcp-server.md b/docs/zeus-mcp-server.md index 3ceae1d0..16e82e19 100644 --- a/docs/zeus-mcp-server.md +++ b/docs/zeus-mcp-server.md @@ -10,11 +10,13 @@ Implementation lives in two places: ## Subcommand surface ```text -zeus mcp [--transport stdio|sse] [--port N] [--workspace PATH] +zeus mcp [--transport stdio|sse] [--port N] [--bind IP] [--allow-non-loopback] [--workspace PATH] ``` - `--transport`: defaults to `stdio` (Anthropic / Claude Code CLI standard). `sse` for HTTP clients. - `--port`: only meaningful with `--transport sse`. Defaults to a random ephemeral port; the chosen port is printed on stderr (stdout is reserved for protocol traffic on the stdio transport, and we keep stderr consistent across transports). +- `--bind`: only meaningful with `--transport sse`. Defaults to `127.0.0.1` (loopback). Accepts IP literals only — pass `127.0.0.1` / `::1` for loopback, not `localhost`. Any non-loopback value additionally requires `--allow-non-loopback`. +- `--allow-non-loopback`: acknowledges that binding the SSE transport to a non-loopback interface (`0.0.0.0`, a LAN IP, etc.) is intentional. Without this flag, non-loopback `--bind` values are refused. - `--workspace`: workspace root path. Defaults to `$PWD`. The Rust CLI launches a headless workbench process (or attaches to a running one if available) and proxies MCP traffic.