Skip to main content

max / goingson

go-mcp: add --stdio transport Serve over stdio (newline-delimited JSON-RPC) so Claude can launch it via `claude mcp add go-mcp -- go-mcp --stdio`. Logs move to stderr so stdout stays the clean protocol channel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-13 19:23 UTC
Commit: 22921e509651d1fad28834c2dc93173a423c37d1
Parent: 1124a30
1 file changed, +16 insertions, -2 deletions
@@ -30,6 +30,7 @@ struct Args {
30 30 grants: HashSet<String>,
31 31 grant_all: bool,
32 32 compact: bool,
33 + stdio: bool,
33 34 }
34 35
35 36 impl Args {
@@ -40,6 +41,7 @@ impl Args {
40 41 let mut grants = HashSet::new();
41 42 let mut grant_all = false;
42 43 let mut compact = false;
44 + let mut stdio = false;
43 45
44 46 let mut it = std::env::args().skip(1);
45 47 while let Some(arg) = it.next() {
@@ -56,6 +58,7 @@ impl Args {
56 58 }
57 59 "--grant-all" => grant_all = true,
58 60 "--compact" => compact = true,
61 + "--stdio" => stdio = true,
59 62 "-h" | "--help" => {
60 63 print_help();
61 64 std::process::exit(0);
@@ -63,7 +66,7 @@ impl Args {
63 66 other => return Err(format!("unknown argument: {other}")),
64 67 }
65 68 }
66 - Ok(Self { db, host, port, grants, grant_all, compact })
69 + Ok(Self { db, host, port, grants, grant_all, compact, stdio })
67 70 }
68 71 }
69 72
@@ -74,8 +77,10 @@ fn next(it: &mut impl Iterator<Item = String>, flag: &str) -> Result<String, Str
74 77 fn print_help() {
75 78 eprintln!(
76 79 "go-mcp — MCP server for GoingsOn tasks/projects\n\n\
77 - Usage: go-mcp [--db <path>] [--host <ip>] [--port <n>]\n\
80 + Usage: go-mcp [--db <path>] [--stdio] [--host <ip>] [--port <n>]\n\
78 81 \x20 [--grant <cap>]... | [--grant-all] [--compact]\n\n\
82 + Transport: default is Streamable HTTP; --stdio speaks newline-delimited\n\
83 + \x20 JSON-RPC over stdin/stdout (for `claude mcp add go-mcp -- go-mcp --stdio`).\n\n\
79 84 Write capabilities (grant to enable the matching tool):\n\
80 85 \x20 go.project.create go.task.create go.task.bulk_import\n\
81 86 \x20 go.task.update go.task.complete\n"
@@ -84,7 +89,10 @@ fn print_help() {
84 89
85 90 #[tokio::main]
86 91 async fn main() -> Result<(), Box<dyn std::error::Error>> {
92 + // Logs go to stderr: in --stdio mode stdout is the protocol channel and any
93 + // stray byte there corrupts the JSON-RPC stream.
87 94 tracing_subscriber::fmt()
95 + .with_writer(std::io::stderr)
88 96 .with_env_filter(
89 97 tracing_subscriber::EnvFilter::try_from_default_env()
90 98 .unwrap_or_else(|_| "info,go_mcp=debug".into()),
@@ -136,6 +144,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
136 144 grants: Some(grants),
137 145 };
138 146
147 + if args.stdio {
148 + tracing::info!(db = %db_path.display(), "go-mcp serving over stdio");
149 + server::stdio::serve(registry, config).await?;
150 + return Ok(());
151 + }
152 +
139 153 let bind = format!("{}:{}", args.host, args.port);
140 154 let bound = server::serve(registry, &bind, config).await?;
141 155 tracing::info!(db = %db_path.display(), url = %bound.url(), "go-mcp listening");