Skip to content

Comments

feat(mcp): add streamable-http transport mode#157

Open
Piotr1215 wants to merge 4 commits intoredis:mainfrom
Piotr1215:feat/streamable-http-transport
Open

feat(mcp): add streamable-http transport mode#157
Piotr1215 wants to merge 4 commits intoredis:mainfrom
Piotr1215:feat/streamable-http-transport

Conversation

@Piotr1215
Copy link

Summary

Adds streamable-http as a transport mode for the MCP server, alongside the existing stdio and sse options.

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

  • Add streamable-http as a --mode choice in the MCP CLI
  • Add run_streamable_http_async() override with Redis connection initialization (matching the pattern used by run_sse_async and run_stdio_async)
  • Set stateless_http=True by default so clients that skip the MCP initialization handshake still work (common with subagent/tool-use patterns in Claude Code)
  • Fix SSE handle_sse handler to return Response() instead of None, preventing a TypeError when SSE clients disconnect
  • Pass stateless=True to _mcp_server.run() for SSE connections

Usage

# Start MCP server with streamable HTTP transport
agent-memory mcp --mode streamable-http --port 9000

Context

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

  • Verify agent-memory mcp --mode streamable-http starts and accepts connections
  • Verify agent-memory mcp --mode sse still works (with the Response() fix)
  • Verify agent-memory mcp --mode stdio is unaffected
  • Verify stateless clients can call tools without completing MCP init handshake

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
Copilot AI review requested due to automatic review settings February 22, 2026 11:16
@jit-ci
Copy link

jit-ci bot commented Feb 22, 2026

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.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --mode to include streamable-http and 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 with stateless=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.

Comment on lines +365 to +367
elif mode == "streamable-http":
logger.info(f"Starting MCP server (streamable HTTP) on port {port}\n")
await mcp_app.run_streamable_http_async()
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 319 to +323
@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"]),
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +179 to +184
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()
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

sse = SseServerTransport(self.settings.message_path)

async def handle_sse(request: Request) -> None:
async def handle_sse(request: Request):
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
async def handle_sse(request: Request):
async def handle_sse(request: Request) -> Response:

Copilot uses AI. Check for mistakes.
Atosik and others added 3 commits February 22, 2026 12:26
- 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.
@Piotr1215 Piotr1215 force-pushed the feat/streamable-http-transport branch from 790b2a8 to 6f3fe08 Compare February 22, 2026 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant