re_ghidra_cc_mcp/mcp.rs
1//! `re-ghidra-cc-mcp` — the plugin's MCP server, spoken over stdio.
2//!
3//! Claude Code and OpenCode both launch this binary and talk JSON-RPC to it on
4//! stdin/stdout. It owns no reverse-engineering logic: the server, its 19
5//! tools, the config contract and the embedded Ghidra worker all live in the
6//! shared `ghidra-mcp` crate, so both hosts front exactly the same code. What
7//! this binary contributes is the host-shaped part — reading the host's
8//! settings file (`.claude/re-ghidra-mcp-cc.local.md` on Claude,
9//! `.opencode/re-ghidra-mcp-opencode.local.md` on OpenCode, chosen by
10//! `RE_GHIDRA_HOST`) as the lowest config layer.
11//!
12//! Two rules follow from stdio being the protocol channel, and both are easy to
13//! get wrong:
14//!
15//! 1. **stdout belongs to the protocol.** `serve` logs only to its per-instance
16//! file log; usage and configuration errors go to stderr.
17//! 2. **The process is long-lived.** It stays up until Claude Code closes the
18//! pipe.
19
20#[tokio::main]
21async fn main() {
22 let args: Vec<String> = std::env::args().skip(1).collect();
23
24 // Claude Code launches MCP servers with the workspace as the working
25 // directory, so this is where the project's settings file lives.
26 let project_dir = std::env::current_dir().unwrap_or_default();
27 let file_config =
28 re_ghidra_mcp_cc::load_settings(&project_dir, re_ghidra_mcp_cc::host_from_env());
29
30 let code = ghidra_mcp::cli::dispatch(
31 env!("CARGO_BIN_NAME"),
32 env!("CARGO_PKG_VERSION"),
33 args,
34 file_config,
35 )
36 .await;
37 if code != 0 {
38 std::process::exit(code);
39 }
40}