feat(mcp): add streamable-http transport mode#157
feat(mcp): add streamable-http transport mode#157Piotr1215 wants to merge 4 commits intoredis:mainfrom
Conversation
MCP clients like Claude Code connect over HTTP but need the streamable-http transport -- not SSE or stdio. Without this mode, deploying agent-memory-server as a network service (e.g. in Kubernetes) requires a custom patch. Changes: - Add "streamable-http" as a --mode choice in the MCP CLI - Add run_streamable_http_async() with Redis connection init - Set stateless_http=True so clients that skip the MCP init handshake (common with subagent/tool-use patterns) still work - Fix SSE handler to return Response() instead of None, preventing TypeError when SSE clients disconnect - Pass stateless=True to _mcp_server.run() for SSE connections
|
Hi, I’m Jit, a friendly security platform designed to help developers build secure applications from day zero with an MVS (Minimal viable security) mindset. In case there are security findings, they will be communicated to you as a comment inside the PR. Hope you’ll enjoy using Jit. Questions? Comments? Want to learn more? Get in touch with us. |
There was a problem hiding this comment.
Pull request overview
Adds support for running the MCP server over the streamable-http transport (in addition to existing stdio and sse) to enable HTTP-based MCP clients to connect without maintaining a patched deployment.
Changes:
- Extend MCP CLI
--modeto includestreamable-httpand route to the appropriate runner. - Add
run_streamable_http_async()override to initialize Redis before starting the Streamable HTTP server. - Adjust SSE handler to return a
Response()and run SSE connections withstateless=True.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
agent_memory_server/mcp.py |
Sets default stateless_http, fixes SSE handler return type/behavior, and adds Streamable HTTP runner with Redis init. |
agent_memory_server/cli.py |
Adds streamable-http as a valid --mode choice and wires CLI dispatch to the new runner. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| elif mode == "streamable-http": | ||
| logger.info(f"Starting MCP server (streamable HTTP) on port {port}\n") | ||
| await mcp_app.run_streamable_http_async() |
There was a problem hiding this comment.
streamable-http mode is added here, but there are existing CLI tests for stdio and sse modes (see tests/test_cli.py::TestMcpCommand). Please add a matching test case that --mode streamable-http calls mcp_app.run_streamable_http_async() and validates logging/backend behavior, to prevent regressions in the new mode selection logic.
| @click.option( | ||
| "--mode", | ||
| default="stdio", | ||
| help="Run the MCP server in SSE or stdio mode", | ||
| type=click.Choice(["stdio", "sse"]), | ||
| help="Run the MCP server in SSE, streamable-http, or stdio mode", | ||
| type=click.Choice(["stdio", "sse", "streamable-http"]), |
There was a problem hiding this comment.
The CLI help/choices are updated to include streamable-http, but the repo documentation still describes MCP --mode as [stdio|sse] (e.g., docs/cli.md, docs/getting-started.md, docs/quick-start.md). Please update those docs (and any examples) to include streamable-http so users don’t follow outdated instructions.
| async def run_streamable_http_async(self): | ||
| """Start streamable HTTP MCP server.""" | ||
| from agent_memory_server.utils.redis import get_redis_conn | ||
|
|
||
| await get_redis_conn() | ||
| return await super().run_streamable_http_async() |
There was a problem hiding this comment.
FastMCP advertises “optional URL namespace” support, but streamable-http currently just delegates to super().run_streamable_http_async() without an app override like sse_app(). As a result, the namespace-in-URL behavior (and call_tool()’s namespace injection via request.path_params) is likely not supported for streamable HTTP. Consider adding a custom Streamable HTTP Starlette app with /{namespace} routes (and setting request context per request) to keep behavior consistent across transports.
There was a problem hiding this comment.
Good point. The streamable-http mode currently delegates to FastMCP's built-in implementation which doesn't include the namespace-in-URL routing. For my use case (single namespace via env var) this works, but I agree parity with SSE's /{namespace} routes would be valuable. Happy to add a custom streamable_http_app() method with namespace routes in a follow-up if the maintainers want it in this PR.
agent_memory_server/mcp.py
Outdated
| sse = SseServerTransport(self.settings.message_path) | ||
|
|
||
| async def handle_sse(request: Request) -> None: | ||
| async def handle_sse(request: Request): |
There was a problem hiding this comment.
handle_sse now returns a Response, but the function signature no longer advertises what it returns. Adding an explicit return type (e.g., -> Response) would make the Starlette contract clearer and help static analysis catch mismatches in the future.
| async def handle_sse(request: Request): | |
| async def handle_sse(request: Request) -> Response: |
- Add return type hint `-> Response` to handle_sse - Update docs/cli.md with streamable-http mode and usage example - Add CLI tests for streamable-http: basic mode, asyncio default, docket backend (matching existing sse/stdio test patterns)
…sions core_get_working_memory returns None when no session exists. the mcp tool declares -> WorkingMemory return type, so the mcp sdk tries pydantic model_validate(None) which fails with "expected string or bytes-like object, got 'Header'" on streamable-http transport.
…sions the mcp get_working_memory handler imported the fastapi route handler from api.py which has a starlette Header() default parameter for x_client_version. when called from mcp (not http), the Header object is passed as-is to re.match() causing "expected string or bytes-like object, got 'Header'". fix: call working_memory.get_working_memory (the core function) directly instead of the api route handler, and guard the None return with an empty WorkingMemory.
790b2a8 to
6f3fe08
Compare
Summary
Adds
streamable-httpas a transport mode for the MCP server, alongside the existingstdioandsseoptions.MCP clients like Claude Code connect over HTTP and expect the streamable-http transport. Without this mode, deploying agent-memory-server as a network service (e.g. in Kubernetes) requires maintaining a custom patched image.
Changes
streamable-httpas a--modechoice in the MCP CLIrun_streamable_http_async()override with Redis connection initialization (matching the pattern used byrun_sse_asyncandrun_stdio_async)stateless_http=Trueby default so clients that skip the MCP initialization handshake still work (common with subagent/tool-use patterns in Claude Code)handle_ssehandler to returnResponse()instead ofNone, preventing aTypeErrorwhen SSE clients disconnectstateless=Trueto_mcp_server.run()for SSE connectionsUsage
# Start MCP server with streamable HTTP transport agent-memory mcp --mode streamable-http --port 9000Context
I'm running agent-memory-server on a homelab Kubernetes cluster with three pods (API, MCP, worker). Claude Code connects to the MCP pod over HTTP via an ingress. Currently maintaining a custom image with these patches applied -- would be great to have this upstream so the custom build isn't needed.
Test plan
agent-memory mcp --mode streamable-httpstarts and accepts connectionsagent-memory mcp --mode ssestill works (with the Response() fix)agent-memory mcp --mode stdiois unaffected