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 d4cefb066a
commit ecadb66414
15 changed files with 2166 additions and 12 deletions
@@ -0,0 +1,36 @@
{% from "_macros.html" import icon %}
{#
One MCP server in the list.
Swapped in place by the “Test & refresh” button, so this fragment has to be
able to render on its own as well as inside the list.
#}
<div class="model-row {{ 'is-off' if not server.enabled }}" id="mcp-{{ server.id }}">
<div class="model-row__main">
<div class="model-row__title">
<a class="model-row__name" href="/admin/mcp/{{ server.id }}/edit">{{ server.name }}</a>
<span class="badge">{{ tool_count }} tool{{ '' if tool_count == 1 else 's' }}</span>
{% if not server.enabled %}<span class="badge badge--danger">disabled</span>{% endif %}
{% if not server.public %}<span class="badge">restricted</span>{% endif %}
{% if server.allow_private %}<span class="badge">private network</span>{% endif %}
{% if server.protocol_version %}
<span class="badge badge--leaf">MCP {{ server.protocol_version }}</span>
{% endif %}
</div>
<code class="model-row__id">{{ server.slug }} · {{ server.url }}</code>
{% if message %}
<p class="text-xs {{ 'danger' if message_kind == 'error' else 'faint' }}">{{ message }}</p>
{% elif server.last_error %}
<p class="text-xs danger">{{ server.last_error }}</p>
{% endif %}
</div>
<div class="model-row__actions">
<button class="btn btn--sm" type="button"
hx-post="/admin/mcp/{{ server.id }}/test"
hx-target="#mcp-{{ server.id }}" hx-swap="outerHTML">
{{ icon("refresh", "icon--sm") }} Test &amp; refresh
</button>
<a class="btn btn--sm" href="/admin/mcp/{{ server.id }}/edit">Edit</a>
</div>
</div>