Skip to content

feat(mcp): bot as MCP client over SSE — minimal stdlib client + 2 LLM tools#12

Open
Smilez1985 wants to merge 1 commit into
turmyshevd:mainfrom
Smilez1985:feat/bot-mcp-client
Open

feat(mcp): bot as MCP client over SSE — minimal stdlib client + 2 LLM tools#12
Smilez1985 wants to merge 1 commit into
turmyshevd:mainfrom
Smilez1985:feat/bot-mcp-client

Conversation

@Smilez1985
Copy link
Copy Markdown
Contributor

Summary

Lets openclawgotchi consume tools advertised by any external MCP server (SSE transport) — the agent can call them via two new generic LLM tools, no per-server adapters required.

The implementation is a hand-rolled minimal MCP-over-SSE client (~250 LoC, stdlib + requests) instead of the official mcp[cli] Python package. Rationale: the official package pulls in cryptography (~4.7 MB), pydantic-settings, starlette, uvicorn, pyjwt, httpx-sse, sse-starlette, python-multipart — non-trivial RAM hit on a 512 MB Pi Zero 2W. The hand-rolled client speaks just the subset needed: initialize, tools/list, tools/call.

This is a follow-up to PR #10 (RAG REST integration) — same RAG_API_URL env, with a new RAG_TRANSPORT switch that picks REST or MCP.

What's added

src/llm/rag_mcp_client.py

  • Sync MCPSSEClient class with .connect() / .initialize() / .list_tools() / .call_tool(name, args).
  • Background thread reads the SSE stream once per process and routes JSON-RPC responses to the calling thread by id (threading.Event per request).
  • Module-level get_client() returns a connected, initialized singleton on first call; subsequent calls reuse the existing connection.
  • extract_text_content(result) flattens an MCP tools/call result to a printable string for the LLM.

Two new LLM tools (litellm_connector.py)

  • mcp_list_tools() — discovery. Returns a compact list name: description (first line).
  • mcp_call_tool(name, arguments) — invoke a tool by name. arguments is a JSON object encoded as a string (the LLM emits one).

Both no-op gracefully (informative return string, no raise) when RAG_TRANSPORT != "mcp", when RAG_API_URL is empty, or when the server is unreachable. Existing installs are unaffected.

Config

  • New env var RAG_TRANSPORT=rest|mcp (default rest).
  • When mcp, RAG_API_URL is interpreted as the MCP-SSE base URL (e.g. http://your-rag-host:8766).
  • Reuses existing RAG_API_KEY for optional Bearer auth.
  • No new top-level dependencies — requests already pulled in by litellm.

Test plan

  • Without RAG_TRANSPORT (or =rest): mcp_list_tools() returns "MCP transport not enabled". Bot otherwise unaffected.
  • With RAG_TRANSPORT=mcp + RAG_API_URL=<sse-base>: mcp_list_tools() lists the server's tools.
  • mcp_call_tool("rag_search", '{"query":"hello","top_k":2}') (or whatever is advertised) returns a result.
  • Server unreachable mid-call: returns "MCP call_tool(...) failed: …", no traceback.
  • Multiple concurrent calls share the singleton connection (verify with ss -tn — only one outbound connection to port 8766).

Verified locally against an MCP-SSE server: tools/list returns the advertised catalogue, tools/call round-trips JSON-RPC and the bot renders the text content correctly.

Notes for reviewers

🤖 Generated with Claude Code

Lets the bot consume tools advertised by any external MCP server
that speaks the SSE transport, without dragging in the official
`mcp[cli]` Python package (it pulls `cryptography`, `pydantic-settings`,
`starlette`, `uvicorn`, `pyjwt`, `httpx-sse`, `sse-starlette`,
`python-multipart` — non-trivial RAM hit on a 512 MB Pi Zero 2W).

What's added
- src/llm/rag_mcp_client.py — hand-rolled MCP-over-SSE client,
  ~250 LoC, stdlib-only + `requests` (already in the venv via
  litellm). Background thread reads the SSE stream and dispatches
  JSON-RPC responses by id; sync `connect()` / `initialize()` /
  `list_tools()` / `call_tool(name, args)` API. A module-level
  `get_client()` returns a lazy singleton so multiple tool calls
  share one SSE connection.
- Two new LLM tools wired into TOOL_MAP:
    `mcp_list_tools()` — return advertised tool names + descriptions
    `mcp_call_tool(name, arguments)` — invoke by name; arguments is
                                       a JSON object passed as a string
                                       (the LLM emits one).
  Both gracefully no-op when the MCP path isn't configured, returning
  a clear hint instead of raising.

Activation
- Env var `RAG_TRANSPORT=rest|mcp` (default `rest`). When `mcp`,
  `RAG_API_URL` is interpreted as the MCP-SSE base URL (e.g.
  `http://your-rag-host:8766`).
- Reuses `RAG_API_KEY` for optional Bearer auth.
- No new top-level dependencies.

Tested against rag-core's MCP-SSE endpoint
(advertised tools: rag_search, rag_persist, rag_status,
 rag_list_collections, rag_recall_session, rag_session_announce,
 rag_session_forget). `tools/list` returns the catalog; `tools/call`
 dispatches and returns the rendered text content correctly.

Out of scope (separate follow-ups)
- Auto-registration of advertised MCP tools as first-class TOOL_MAP
  entries (each with its own typed JSON schema). Today the LLM has
  to look at `mcp_list_tools` then construct an `mcp_call_tool` call
  itself; auto-registration would let it call them as if native.
- Multi-server support (today: single MCP server via RAG_API_URL).
- Async transport / WebSocket fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Smilez1985
Copy link
Copy Markdown
Contributor Author

Smilez1985 commented May 9, 2026

Quick context on this integration — wanted to be upfront about where it comes from.

The RAG backend I built against is a project I call rag-core: a self-hosted, project-scoped retrieval memory layer with a REST API and an MCP-SSE gateway (Qdrant-backed, multi-collection, frontmatter-aware chunking, designed for AI agents). The repo is currently private while I finish a handful of pre-public-readiness items, but the plan is to open it up before long.

This PR series (#10, #12, #13, #14) is intentionally written against a generic contract — any RAG backend that speaks the documented REST shape, and any MCP-SSE server with tools/list + tools/call. Nothing here pins to rag-core specifically; it just happens to be the implementation I run.

Reason for sending this upstream rather than keeping it on my fork: I use OpenClawGotchi as one of my main development drivers, and the RAG integration has become part of how I work with it day-to-day. Carrying a long-lived branch and re-rebasing on every upstream pull is a fair amount of churn — having it on main would save a meaningful amount of repeat work on my side.

No pressure on accepting; happy to iterate to whatever shape fits best, or to wait if you'd rather see the other end of the wire first. Once rag-core is public I'll ping back here — at that point you'd be welcome to clone the repo and stand up your own instance if you want to see how it pairs with the bot from the other side. (Just to be clear: this isn't an invite to my running instance — it's an invite to the source, so you can run your own.)

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eb703a14-7cc3-42b0-819e-51b1964009b7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

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