feat(http): add Streamable HTTP transport with bearer auth (FJMCP-6) #5
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/http-transport-bearer-auth-fjmcp-6"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements FJMCP-6. Adds a Streamable HTTP transport so
forgejo-mcpcan run autonomously as a long-lived, network-reachable process, while leaving the existing stdio launch model untouched.What changed
serveselects its transport at startup fromMCP_TRANSPORT(stdiodefault; unset or unrecognized values fall back to stdio).MCP_TRANSPORT=httpenters the new path insrc/http.rs.StreamableHttpServicebehind an axum router atPOST/GET/DELETE {MCP_HTTP_ADDR}/mcp(default127.0.0.1:8080).ForgejoServerisClone, so the per-session factory clones it and the four tools (whoami,get_repo,list_issues,get_issue) are reused with no changes to their implementations.401for any request without a validAuthorization: Bearer <MCP_HTTP_TOKEN>before it reaches the MCP handler (tokens are hashed before comparison so timing does not leak the secret), and the server refuses to start whenMCP_HTTP_TOKENis unset or empty.README.mddocuments the transport, theMCP_TRANSPORT/MCP_HTTP_ADDR/MCP_HTTP_TOKENvars, and an HTTP-modeclaude mcp addexample.CLAUDE.mdrevises the "No HTTP server" and "only runtime input is FORGEJO_HOST" notes.Test plan
just checkpasses: fmt, clippy (-D warnings),cargo build --all-targets, and the builder-stage Docker compile of the new axum deps in the glibc image.MCP_HTTP_TOKENunset;401for missing and wrong tokens;200+Mcp-Session-Id+initializeresult with the valid token;tools/listreturns all four tools; SIGTERM shuts down cleanly (exit 0).The server only spoke MCP over stdio, so it could not run as a long-lived, network-reachable process. serve now selects its transport at startup from MCP_TRANSPORT: the default (unset or any unrecognized value) keeps the existing stdio path unchanged, while MCP_TRANSPORT=http enters a new Streamable HTTP path in src/http.rs. ForgejoServer is already Clone and stateless beyond the host, so the per-session factory clones it and the tool implementations (whoami, get_repo, list_issues, get_issue) are reused verbatim across both transports. HTTP mode mounts rmcp's StreamableHttpService behind an axum router at POST/GET/DELETE {MCP_HTTP_ADDR}/mcp (default 127.0.0.1:8080). Because the server loads stored Forgejo credentials from keys.json, the listener is authenticated by default: a tower middleware layer rejects any request lacking a valid Authorization: Bearer <MCP_HTTP_TOKEN> with 401 before it reaches the MCP handler, comparing hashed tokens so timing does not leak the secret. It fails closed, refusing to start when MCP_HTTP_TOKEN is unset or empty rather than serving credentialed access unauthenticated, and shuts down gracefully on SIGINT/SIGTERM by cancelling active sessions and draining the listener. README.md documents the new transport, the three env vars, and an HTTP-mode claude mcp add example; CLAUDE.md revises the "No HTTP server" and "only runtime input is FORGEJO_HOST" notes to reflect HTTP mode. Deployment packaging (compose, an OCI runtime image, TLS termination) stays out of scope as a follow-up. #FJMCP-6 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>