MCP servers, over streamable HTTP

A server is a row with a URL; its tools are discovered by a button and
cached, then offered beside the built-in ones. Written by hand rather than
taken from the reference SDK, because that SDK's transport does its own
connecting -- and the one thing that must not be bypassed is check_url on
every hop. Owning the transport is the point; the framing beside it is the
small part.

Sessions are per call: initialize, initialized, the call, a best-effort
DELETE. Caching one wants an owner, a TTL, eviction, a lock and a shutdown
hook, and the server may expire it under all of that anyway -- ToolContext
is a session-free snapshot precisely so nothing in a tool holds live state.

A server's names and descriptions reach the model as instructions and are
bounded before they do; what it returns is escaped preformatted text, never
markdown. Tools are namespaced per server, so two servers exposing "search"
do not collide and neither shadows a built-in.

Also: a round's calls now run together under a semaphore, results indexed
so each tool turn stays paired with its call, and generation.status names
what is running -- a remote tool is latency-bound, and a silent pause is
what a hang looks like.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-01 16:44:29 +02:00
parent bc84fec21d
commit ecb52e9978
17 changed files with 2270 additions and 28 deletions
+26
View File
@@ -0,0 +1,26 @@
"""An MCP client: remote servers over streamable HTTP.
Three parts. `protocol` is the wire format and nothing else -- it knows no
database and no HTTP. `client` owns the transport, which is where the SSRF guard
lives and the reason none of this comes from the reference SDK. `registry` is
where a row becomes a tool the chat loop can be offered.
Local stdio servers are deliberately absent. Spawning a subprocess is the
agentic-execution feature, which wants a confirmation model before it does
anything; a URL is a different act with a different blast radius.
"""
from lembas.services.mcp.client import McpSpec, call_tool, list_tools, spec_from
from lembas.services.mcp.protocol import McpError
from lembas.services.mcp.registry import offer_name, refresh, tool_defs
__all__ = [
"McpError",
"McpSpec",
"call_tool",
"list_tools",
"offer_name",
"refresh",
"spec_from",
"tool_defs",
]