Skip to content

Commit 2b0ad97

Browse files
committed
Fix(mcp): improve remote HTTP transport compatibility
1 parent 496309e commit 2b0ad97

11 files changed

Lines changed: 502 additions & 157 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ All notable changes to this project will be documented in this file.
77
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
88
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

10+
## [0.21.18] - 2026-05-28
11+
12+
### Fixed
13+
14+
- **Remote MCP HTTP compatibility** - HTTP MCP endpoints without an explicit transport now try Streamable HTTP first and fall back to legacy SSE only when the connection fails before initialization, improving compatibility with stricter SSE servers while preserving explicit `type` settings and per-server headers.
15+
- **MCP transport config compatibility** - existing MCP configs and management clients can continue using legacy `transport` fields and values such as `sse/http` or `streamable-http`, which are now normalized to Facio's canonical transport settings during config load.
16+
- **Placet review message ordering** - active Placet response streams are now closed before review prompts are sent, keeping approval and HITL messages ordered separately from the draft assistant stream.
17+
1018
## [0.21.17] - 2026-05-27
1119

1220
### Fixed

facio/agent/loop.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
RUNTIME_CHECKPOINT_KEY,
3535
SessionCheckpointManager,
3636
)
37+
from facio.agent.stream_boundary import reset_stream_boundary_hook, set_stream_boundary_hook
3738
from facio.agent.stream_session import StreamSession
3839
from facio.agent.subagent import SubagentManager
3940
from facio.agent.tools.file_state import FileStateStore, bind_file_states, reset_file_states
@@ -1055,6 +1056,7 @@ async def _dispatch(self, msg: InboundMessage) -> None:
10551056
)
10561057
_run_status: dict[str, Any] = {}
10571058
_run_t0 = time.monotonic()
1059+
_stream_boundary_token = set_stream_boundary_hook(None)
10581060

10591061
lock = self._session_locks.setdefault(session_key, asyncio.Lock())
10601062
gate = self._concurrency_gate or nullcontext()
@@ -1096,6 +1098,10 @@ async def _dispatch(self, msg: InboundMessage) -> None:
10961098
mt, "set_pre_send_hook"
10971099
):
10981100
mt.set_pre_send_hook(stream.finalize_and_rotate)
1101+
reset_stream_boundary_hook(_stream_boundary_token)
1102+
_stream_boundary_token = set_stream_boundary_hook(
1103+
stream.finalize_and_rotate
1104+
)
10991105

11001106
# Send immediate "Thinking" progress so the frontend shows
11011107
# an indicator before the LLM call starts.
@@ -1167,6 +1173,7 @@ async def _dispatch(self, msg: InboundMessage) -> None:
11671173
# Restore active status after processing
11681174
await self._runtime.send_status(agent_status.ACTIVE)
11691175
finally:
1176+
reset_stream_boundary_hook(_stream_boundary_token)
11701177
self._runtime.discard_rotator(session_key)
11711178
# Clear the pre-send hook we may have installed on MessageTool
11721179
# so the next (possibly non-streaming) turn starts with a clean

facio/agent/stream_boundary.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Context-local stream boundary hook for user-visible side messages.
2+
3+
Tools that emit their own Placet messages while the model is streaming need
4+
to close the current draft before they send, then let the agent loop rotate to
5+
a fresh stream id. This keeps the chat timeline ordered without coupling each
6+
tool to ``AgentLoop`` or ``StreamSession`` directly.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from collections.abc import Awaitable, Callable
12+
from contextvars import ContextVar, Token
13+
14+
StreamBoundaryHook = Callable[[], Awaitable[None]]
15+
16+
_HOOK: ContextVar[StreamBoundaryHook | None] = ContextVar(
17+
"facio_stream_boundary_hook",
18+
default=None,
19+
)
20+
21+
22+
def set_stream_boundary_hook(hook: StreamBoundaryHook | None) -> Token[StreamBoundaryHook | None]:
23+
"""Set the hook for the current execution context."""
24+
return _HOOK.set(hook)
25+
26+
27+
def reset_stream_boundary_hook(token: Token[StreamBoundaryHook | None]) -> None:
28+
"""Restore the previous hook for the current execution context."""
29+
_HOOK.reset(token)
30+
31+
32+
async def finalize_and_rotate_current_stream() -> bool:
33+
"""Run the active stream boundary hook, if this context has one."""
34+
hook = _HOOK.get()
35+
if hook is None:
36+
return False
37+
await hook()
38+
return True

facio/agent/tools/manage_mcp.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,20 @@
2626
),
2727
config=StringSchema(
2828
"JSON string with server config (required for 'add'). "
29-
"Fields: command, args, env, url, headers, tool_timeout, enabled_tools. "
30-
"Example: {\"command\": \"npx\", \"args\": [\"-y\", \"@example/mcp-server\"]}",
29+
"Fields: command, args, env, url, type, headers, tool_timeout, enabled_tools, enabled. "
30+
"For ambiguous HTTP endpoints, omit type to allow automatic streamableHttp then SSE fallback. "
31+
"Set type to 'sse' or 'streamableHttp' only to force a transport. "
32+
"Headers remain per-server; for example "
33+
"{\"url\": \"https://example.com/mcp\", "
34+
"\"headers\": {\"Authorization\": \"Bearer ...\"}}. "
35+
"Stdio example: {\"command\": \"npx\", \"args\": [\"-y\", \"@example/mcp-server\"]}",
3136
nullable=True,
3237
),
3338
updates=StringSchema(
3439
"JSON string with fields to update (for 'edit'). "
35-
"Example: {\"tool_timeout\": 60, \"enabled_tools\": [\"*\"]}",
40+
"Use this for targeted per-server compatibility overrides such as "
41+
"{\"type\": \"sse\", \"headers\": {\"Accept\": \"text/event-stream\"}} "
42+
"or {\"tool_timeout\": 60, \"enabled_tools\": [\"*\"]}.",
3643
nullable=True,
3744
),
3845
confirmed=StringSchema(
@@ -67,6 +74,8 @@ def description(self) -> str:
6774
"'edit' updates server config (requires name + updates JSON), "
6875
"'enable'/'disable' toggles a server on/off, "
6976
"'restart' reconnects a server. "
77+
"For HTTP MCP compatibility, leave type unset for automatic streamableHttp/SSE fallback "
78+
"or use per-server type and headers to force one transport. "
7079
"For credentials, use the separate 'manage_credentials' tool."
7180
)
7281

0 commit comments

Comments
 (0)