Skip to content

Commit 2e6a47f

Browse files
authored
feat(mcp): authenticated read-only Streamable HTTP preview (#518)
## Summary Adds an opt-in Streamable HTTP endpoint using the existing RMCP SDK and Sem handlers. Stdio remains unchanged. - `sem mcp --http 127.0.0.1:8765` (also standalone `sem-mcp --http`) - Explicit SEM_REPO, mandatory random bearer token, loopback-only bind, exact Host check, rejects browser Origin requests - Only sem_find and sem_grep advertised AND callable; no validation commands, cloud tools, or Weave writes - Shared repository caches, stateless requests with independent context ledgers - 1 MiB body limit and eight concurrent HTTP requests; relative-path and explicit symlink escape checks - Source-build documentation, release limitation, and smoke test in existing Linux/macOS CI ## Validation - Built sem-cli and sem-mcp locally - 100 sem-mcp library tests passed - 12 existing MCP protocol tests passed - 7 live HTTP smoke tests passed (handshake, reads, lookup/grep, hidden-tool denial, paths, auth/Host/Origin/body size, nonloopback rejection) - Actual LangChain MCP adapter 0.2.1/MCP 1.25 and Aser/FastMCP 4.1 clients retrieved a function and caller over HTTP ## Deliberate boundaries This is a trusted-client local/SSH-tunnel preview, NOT public hosting, OAuth, a multi-tenant sandbox, or a remote editing service. It does not make arbitrary checkouts safe. Published 0.27.0 does not include this feature. No speedup claim. Downstream examples will be drafts pending this transport landing. AI-assisted implementation; local toolchain emitted existing LLVM debug-stripping warnings but builds and tests completed successfully.
1 parent 0bbe3d5 commit 2e6a47f

11 files changed

Lines changed: 585 additions & 8 deletions

File tree

‎.github/workflows/shared-mcp.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ jobs:
1919
workspaces: crates
2020
- run: cargo build --manifest-path crates/Cargo.toml -p sem-mcp -p sem-cli
2121
- run: cargo test --manifest-path crates/Cargo.toml -p sem-mcp --lib --tests
22+
- run: python3 crates/sem-mcp/tests/http_smoke.py crates/target/debug/sem-mcp
2223
- run: python3 benchmarks/shared-mcp/run.py --binary crates/target/debug/sem-mcp

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to sem are documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
- Preview authenticated, loopback-only Streamable HTTP MCP via `sem mcp --http`, exposing read-only `sem_find` and `sem_grep` with shared repository caches. See `docs/mcp-http.md` for source-build instructions and deployment limits.
9+
710
### Added
811

912
- **`sem_find` reads whole files, so an agent never needs Read, `cat` or `sed` to look at code or config.** With mode `context` and no query, it reads `files: [...]` (or the file in `in`) in one call, in parallel, numbered like `cat -n`, with an optional `lines` range. A file the session already read answers in one line when it is unchanged, and with a line diff against the version the agent saw when it changed; `fresh: true` resends it whole.

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,8 @@ This means sem detects renames and moves, not just additions and deletions. Stru
602602

603603
## Use with AI agents (MCP)
604604

605+
Need a URL-based client? The [Streamable HTTP preview](docs/mcp-http.md) adds authenticated, local read-only access; stdio remains the default.
606+
605607
On macOS and Linux, clients in the same checkout share a warm repository daemon.
606608
Run `sem mcp --status` to check it. See the [shared runtime contract and reproducible benchmark](docs/shared-mcp.md)
607609
for session isolation, fallback behavior, and current platform limits.

‎crates/Cargo.lock‎

Lines changed: 104 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/sem-cli/src/main.rs‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,10 @@ enum Commands {
917917
/// Run sem as an MCP server for agents (stdin/stdout): find, grep, impact, check, certify, diff, graph, history
918918
#[command(display_order = 11)]
919919
Mcp {
920+
/// Serve read-only discovery over authenticated Streamable HTTP on loopback.
921+
/// Requires SEM_REPO and SEM_MCP_HTTP_TOKEN (at least 32 characters).
922+
#[arg(long, conflicts_with_all = ["status", "resident", "review"])]
923+
http: Option<std::net::SocketAddr>,
920924
/// Check shared MCP daemon health without starting it (JSON). Example: sem mcp --status
921925
#[arg(long, conflicts_with = "resident")]
922926
status: bool,
@@ -1916,7 +1920,7 @@ fn sem_main() {
19161920
alias::note("stats", "config stats", false);
19171921
commands::stats::run();
19181922
}
1919-
Some(Commands::Mcp { resident, status, review }) => {
1923+
Some(Commands::Mcp { resident, status, review, http }) => {
19201924
if status {
19211925
println!("{}", sem_mcp::shared_status());
19221926
return;
@@ -1925,7 +1929,9 @@ fn sem_main() {
19251929
// No-op: see the `resident` field's doc comment above.
19261930
return;
19271931
}
1928-
let result = if review { sem_mcp::run_review() } else { sem_mcp::run() };
1932+
let result = if let Some(address) = http {
1933+
sem_mcp::http::run(address)
1934+
} else if review { sem_mcp::run_review() } else { sem_mcp::run() };
19291935
if let Err(e) = result {
19301936
eprintln!("{} {}", "error:".red().bold(), e);
19311937
std::process::exit(1);

‎crates/sem-mcp/Cargo.toml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ sem-core = { path = "../sem-core", version = "0.27.0", features = ["disk-cache"]
1818
sem-cloud-client = { path = "../sem-cloud-client", version = "0.27.0" }
1919
git2 = "0.20"
2020
lru = "0.16"
21-
rmcp = { version = "1", features = ["server", "transport-io"] }
21+
rmcp = { version = "1", features = ["server", "transport-io", "transport-streamable-http-server"] }
22+
axum = "0.8"
23+
subtle = "2"
24+
tower-http = { version = "0.6", features = ["limit"] }
2225
tokio = { version = "1", features = ["full"] }
2326
serde = { version = "1", features = ["derive"] }
2427
serde_json = "1"
@@ -34,7 +37,6 @@ ureq = { version = "2", features = ["json"] }
3437

3538
[dev-dependencies]
3639
tempfile = "3.24.0"
37-
axum = "0.8"
3840
# The cache test module (src/cache.rs) asserts against raw `rusqlite` types
3941
# directly (schema/row-level checks predating DiskCache move to
4042
# sem-core); production code no longer touches `rusqlite` itself, so this is

0 commit comments

Comments
 (0)