feat(http): add Streamable HTTP transport with bearer auth (FJMCP-6) #5

Merged
David merged 1 commit from feat/http-transport-bearer-auth-fjmcp-6 into main 2026-05-28 12:07:00 +02:00
Owner

Implements FJMCP-6. Adds a Streamable HTTP transport so forgejo-mcp can run autonomously as a long-lived, network-reachable process, while leaving the existing stdio launch model untouched.

What changed

  • serve selects its transport at startup from MCP_TRANSPORT (stdio default; unset or unrecognized values fall back to stdio). MCP_TRANSPORT=http enters the new path in src/http.rs.
  • HTTP mode mounts rmcp's StreamableHttpService behind an axum router at POST/GET/DELETE {MCP_HTTP_ADDR}/mcp (default 127.0.0.1:8080). ForgejoServer is Clone, 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.
  • Bearer-token auth is required and fails closed: a tower middleware layer returns 401 for any request without a valid Authorization: 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 when MCP_HTTP_TOKEN is unset or empty.
  • Graceful shutdown on SIGINT/SIGTERM cancels active sessions and drains the listener. Logs go to stderr in both modes.
  • README.md documents the transport, the MCP_TRANSPORT / MCP_HTTP_ADDR / MCP_HTTP_TOKEN vars, and an HTTP-mode claude mcp add example. CLAUDE.md revises the "No HTTP server" and "only runtime input is FORGEJO_HOST" notes.
  • Out of scope (follow-up): deployment packaging (compose, OCI runtime image, TLS termination), per-tool authorization.

Test plan

  • just check passes: fmt, clippy (-D warnings), cargo build --all-targets, and the builder-stage Docker compile of the new axum deps in the glibc image.
  • Unit tests for the token fail-closed check, address parsing/default, and bearer matching (valid/wrong/missing/wrong-scheme).
  • Live smoke test: fail-closed exit when MCP_HTTP_TOKEN unset; 401 for missing and wrong tokens; 200 + Mcp-Session-Id + initialize result with the valid token; tools/list returns all four tools; SIGTERM shuts down cleanly (exit 0).
  • stdio mode unchanged (default transport, existing README registration still applies).
Implements FJMCP-6. Adds a Streamable HTTP transport so `forgejo-mcp` can run autonomously as a long-lived, network-reachable process, while leaving the existing stdio launch model untouched. ## What changed - `serve` selects its transport at startup from `MCP_TRANSPORT` (`stdio` default; unset or unrecognized values fall back to stdio). `MCP_TRANSPORT=http` enters the new path in `src/http.rs`. - HTTP mode mounts rmcp's `StreamableHttpService` behind an axum router at `POST/GET/DELETE {MCP_HTTP_ADDR}/mcp` (default `127.0.0.1:8080`). `ForgejoServer` is `Clone`, 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. - Bearer-token auth is required and fails closed: a tower middleware layer returns `401` for any request without a valid `Authorization: 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 when `MCP_HTTP_TOKEN` is unset or empty. - Graceful shutdown on SIGINT/SIGTERM cancels active sessions and drains the listener. Logs go to stderr in both modes. - `README.md` documents the transport, the `MCP_TRANSPORT` / `MCP_HTTP_ADDR` / `MCP_HTTP_TOKEN` vars, and an HTTP-mode `claude mcp add` example. `CLAUDE.md` revises the "No HTTP server" and "only runtime input is FORGEJO_HOST" notes. - Out of scope (follow-up): deployment packaging (compose, OCI runtime image, TLS termination), per-tool authorization. ## Test plan - [x] `just check` passes: fmt, clippy (`-D warnings`), `cargo build --all-targets`, and the builder-stage Docker compile of the new axum deps in the glibc image. - [x] Unit tests for the token fail-closed check, address parsing/default, and bearer matching (valid/wrong/missing/wrong-scheme). - [x] Live smoke test: fail-closed exit when `MCP_HTTP_TOKEN` unset; `401` for missing and wrong tokens; `200` + `Mcp-Session-Id` + `initialize` result with the valid token; `tools/list` returns all four tools; SIGTERM shuts down cleanly (exit 0). - [x] stdio mode unchanged (default transport, existing README registration still applies).
feat(http): add Streamable HTTP transport with bearer auth (FJMCP-6)
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 31s
Create release / Create release from merged PR (pull_request) Has been skipped
4dfced174b
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>
David merged commit d10b9d88de into main 2026-05-28 12:07:00 +02:00
David deleted branch feat/http-transport-bearer-auth-fjmcp-6 2026-05-28 12:07:00 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pandoras-box/forgejo-mcp!5
No description provided.