Skip to content

Commit c073d30

Browse files
committed
deprecate-sse
Signed-off-by: yaacov <yzamir@redhat.com>
1 parent ac33951 commit c073d30

12 files changed

Lines changed: 49 additions & 45 deletions

File tree

docs/architecture.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ and `stop` commands. If you run MCP servers yourself, configure their URLs in
2727
│ (browser)│ │ (FastAPI) │ │(LM Studio / │
2828
└──────────┘ └──────┬───────┘ │ Claude) │
2929
│ └─────────────┘
30-
MCP SSE
30+
MCP HTTP
3131
┌──────────────┼──────────────┐
3232
│ │ │
3333
┌────▼────┐ ┌─────▼─────┐ ┌────▼────────┐
@@ -87,13 +87,13 @@ virtual tools that do not call external services:
8787

8888
## MCP integration
8989

90-
MCP servers expose tools over SSE (Server-Sent Events). The `MCPManager`
91-
connects to each server defined in `mcp.json`, discovers available tools, and
92-
registers them in the `ToolRegistry`.
90+
MCP servers expose tools over Streamable HTTP. The `MCPManager` connects to
91+
each server defined in `mcp.json`, discovers available tools, and registers
92+
them in the `ToolRegistry`.
9393

9494
When `kubeAuth` is `true` for a server entry, the agent automatically injects
9595
`Authorization` (bearer token) and `X-Kubernetes-Server` (API URL) headers
96-
into every SSE request. This is how the MCP containers access the OpenShift
96+
into every MCP request. This is how the MCP containers access the OpenShift
9797
cluster without needing their own kubeconfig.
9898

9999
## Skills and playbooks

docs/configuration.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,17 @@ mtv-agent config
116116
{
117117
"mcpServers": {
118118
"kubectl-mtv": {
119-
"url": "http://localhost:8080/sse",
119+
"url": "http://localhost:8080/mcp",
120120
"image": "quay.io/yaacov/kubectl-mtv-mcp-server:latest",
121121
"kubeAuth": true
122122
},
123123
"kubectl-metrics": {
124-
"url": "http://localhost:8081/sse",
124+
"url": "http://localhost:8081/mcp",
125125
"image": "quay.io/yaacov/kubectl-metrics-mcp-server:latest",
126126
"kubeAuth": true
127127
},
128128
"kubectl-debug-queries": {
129-
"url": "http://localhost:8082/sse",
129+
"url": "http://localhost:8082/mcp",
130130
"image": "quay.io/yaacov/kubectl-debug-queries-mcp-server:latest",
131131
"kubeAuth": true
132132
}
@@ -138,9 +138,9 @@ mtv-agent config
138138

139139
| Key | Type | Default | Description |
140140
|---|---|---|---|
141-
| `url` | string | *(required)* | SSE endpoint URL for the MCP server |
141+
| `url` | string | *(required)* | Streamable HTTP endpoint URL for the MCP server |
142142
| `image` | string | | Container image used by `mtv-agent start` to run the server |
143-
| `headers` | object | `{}` | Custom HTTP headers sent with every SSE request |
143+
| `headers` | object | `{}` | Custom HTTP headers sent with every MCP request |
144144
| `kubeAuth` | boolean | `false` | Inject auto-resolved Kubernetes credentials (`Authorization` and `X-Kubernetes-Server` headers) |
145145

146146
## Environment variable overrides

docs/development.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ mtv-agent/
128128
│ │ ├── html_to_md.py # HTML-to-markdown conversion
129129
│ │ ├── kubeconfig.py # Kubeconfig credential extraction (via k8s client)
130130
│ │ ├── llm.py # OpenAI-compatible LLM client
131-
│ │ ├── mcp_client.py # MCP SSE client
131+
│ │ ├── mcp_client.py # MCP Streamable HTTP client
132132
│ │ ├── mcp_manager.py # Multi-MCP server manager
133133
│ │ ├── md_sections.py # Markdown section utilities
134134
│ │ ├── memory.py # In-memory conversation history

docs/quickstart.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,9 @@ You should see output like:
117117

118118
```
119119
INFO Starting MCP servers (docker)...
120-
INFO mtv-agent-mcp-mtv -> http://localhost:8080/sse
121-
INFO mtv-agent-mcp-metrics -> http://localhost:8081/sse
122-
INFO mtv-agent-mcp-debug-queries -> http://localhost:8082/sse
120+
INFO mtv-agent-mcp-mtv -> http://localhost:8080/mcp
121+
INFO mtv-agent-mcp-metrics -> http://localhost:8081/mcp
122+
INFO mtv-agent-mcp-debug-queries -> http://localhost:8082/mcp
123123
INFO Starting API server...
124124
INFO Uvicorn running on http://0.0.0.0:8000
125125
```

mcp.json.example

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
{
22
"mcpServers": {
33
"kubectl-mtv": {
4-
"url": "http://localhost:8080/sse",
4+
"url": "http://localhost:8080/mcp",
55
"image": "quay.io/yaacov/kubectl-mtv-mcp-server:latest",
66
"kubeAuth": true
77
},
88
"kubectl-metrics": {
9-
"url": "http://localhost:8081/sse",
9+
"url": "http://localhost:8081/mcp",
1010
"image": "quay.io/yaacov/kubectl-metrics-mcp-server:latest",
1111
"kubeAuth": true
1212
},
1313
"kubectl-debug-queries": {
14-
"url": "http://localhost:8082/sse",
14+
"url": "http://localhost:8082/mcp",
1515
"image": "quay.io/yaacov/kubectl-debug-queries-mcp-server:latest",
1616
"kubeAuth": true
1717
}

mtv_agent/config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,15 +190,15 @@ def _flatten_config(data: dict[str, Any]) -> dict[str, Any]:
190190

191191
@dataclass
192192
class MCPServerConfig:
193-
"""Connection details for a single MCP SSE server."""
193+
"""Connection details for a single MCP Streamable HTTP server."""
194194

195195
url: str
196196
headers: dict[str, str] = field(default_factory=dict)
197197
kube_auth: bool = False
198198

199199

200200
def build_kube_auth_headers(api_url: str, token: str) -> dict[str, str]:
201-
"""Build HTTP headers for MCP SSE auth from Kubernetes credentials.
201+
"""Build HTTP headers for MCP auth from Kubernetes credentials.
202202
203203
Returns an empty dict when both values are blank.
204204
"""
@@ -235,7 +235,7 @@ def inject_kube_headers(
235235

236236

237237
def load_mcp_servers(data: dict[str, Any]) -> dict[str, MCPServerConfig]:
238-
"""Extract MCP SSE server entries from a parsed MCP config dict.
238+
"""Extract MCP server entries from a parsed MCP config dict.
239239
240240
Reads the top-level ``mcpServers`` key. Entries that use ``command``
241241
(stdio transport) are skipped with a warning.

mtv_agent/data/skills/mtv-cli-docs/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ kubectl mtv delete plan --name my-mig --clean-all -n <ns>
162162
| 19. Plan Lifecycle | [19-lifecycle](https://yaacov.github.io/kubectl-mtv/guide/19-plan-lifecycle-execution) | Start, cutover, cancel, archive, unarchive plans |
163163
| 20. Troubleshooting | [20-debugging](https://yaacov.github.io/kubectl-mtv/guide/20-debugging-and-troubleshooting) | Debug output, common issues, convertor pods stuck, mapping errors |
164164
| 21. Best Practices | [21-best-practices](https://yaacov.github.io/kubectl-mtv/guide/21-best-practices-and-security) | Plan strategies, provider security, query optimization, RBAC |
165-
| 22. MCP Server | [22-mcp](https://yaacov.github.io/kubectl-mtv/guide/22-model-context-protocol-mcp-server-integration) | AI assistant integration, stdio/SSE modes, Claude/Cursor setup |
165+
| 22. MCP Server | [22-mcp](https://yaacov.github.io/kubectl-mtv/guide/22-model-context-protocol-mcp-server-integration) | AI assistant integration, stdio/HTTP modes, Claude/Cursor setup |
166166
| 23. KubeVirt Tools | [23-kubevirt](https://yaacov.github.io/kubectl-mtv/guide/23-integration-with-kubevirt-tools) | virtctl integration for post-migration VM lifecycle |
167167
| 24. Health Checks | [24-health](https://yaacov.github.io/kubectl-mtv/guide/24-system-health-checks) | `kubectl mtv health`, operator/controller/pod/provider/plan checks |
168168
| 25. Settings | [25-settings](https://yaacov.github.io/kubectl-mtv/guide/25-settings-management) | `settings get/set/unset`, feature flags, performance tuning, VDDK image |

mtv_agent/lib/mcp/mcp_client.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""MCP SSE client -- connect, discover tools, and call them."""
1+
"""MCP Streamable HTTP client -- connect, discover tools, and call them."""
22

33
from __future__ import annotations
44

@@ -11,7 +11,7 @@
1111
import anyio
1212
import httpx
1313
from mcp import ClientSession
14-
from mcp.client.sse import sse_client
14+
from mcp.client.streamable_http import streamable_http_client
1515
from mcp.types import TextContent
1616

1717
logger = logging.getLogger(__name__)
@@ -31,13 +31,13 @@
3131

3232

3333
class MCPClient:
34-
"""Manages a persistent SSE connection to an MCP server.
34+
"""Manages a persistent Streamable HTTP connection to an MCP server.
3535
3636
Each connection runs in a dedicated background task so the anyio
37-
cancel-scope created by ``sse_client`` is tied to that task, not to
38-
the caller (e.g. the FastAPI lifespan handler). This prevents
39-
``CancelledError`` from leaking into the lifespan when a connection
40-
is torn down from a request handler.
37+
cancel-scope created by ``streamable_http_client`` is tied to that
38+
task, not to the caller (e.g. the FastAPI lifespan handler). This
39+
prevents ``CancelledError`` from leaking into the lifespan when a
40+
connection is torn down from a request handler.
4141
"""
4242

4343
def __init__(self, tool_timeout: int = DEFAULT_TOOL_CALL_TIMEOUT) -> None:
@@ -51,7 +51,7 @@ def __init__(self, tool_timeout: int = DEFAULT_TOOL_CALL_TIMEOUT) -> None:
5151
self._connect_error: BaseException | None = None
5252

5353
async def connect(self, url: str, headers: dict[str, str] | None = None) -> None:
54-
"""Establish an SSE session and initialize the MCP handshake."""
54+
"""Establish a Streamable HTTP session and initialize the MCP handshake."""
5555
await self.disconnect()
5656
self._url = url
5757
self._headers = headers
@@ -64,15 +64,19 @@ async def connect(self, url: str, headers: dict[str, str] | None = None) -> None
6464
raise self._connect_error
6565

6666
async def _run(self) -> None:
67-
"""Background task owning the SSE transport lifetime."""
67+
"""Background task owning the Streamable HTTP transport lifetime."""
6868
assert self._ready is not None and self._stop is not None
6969
try:
7070
async with AsyncExitStack() as stack:
71-
read_stream, write_stream = await stack.enter_async_context(
72-
sse_client(
71+
http_client = httpx.AsyncClient(
72+
headers=self._headers or {},
73+
timeout=httpx.Timeout(30, read=60 * 60 * 24),
74+
)
75+
await stack.enter_async_context(http_client)
76+
read_stream, write_stream, _ = await stack.enter_async_context(
77+
streamable_http_client(
7378
self._url,
74-
headers=self._headers,
75-
sse_read_timeout=60 * 60 * 24,
79+
http_client=http_client,
7680
)
7781
)
7882
session = await stack.enter_async_context(
@@ -90,7 +94,7 @@ async def _run(self) -> None:
9094
self._ready.set()
9195

9296
async def _reconnect(self) -> None:
93-
"""Attempt to re-establish the SSE connection."""
97+
"""Attempt to re-establish the Streamable HTTP connection."""
9498
logger.info("Reconnecting MCP client to %s …", self._url)
9599
url, headers = self._url, self._headers
96100
await self.disconnect()
@@ -192,7 +196,7 @@ def url(self) -> str | None:
192196
return self._url
193197

194198
async def disconnect(self) -> None:
195-
"""Tear down the SSE connection (preserves url/headers for reconnect)."""
199+
"""Tear down the connection (preserves url/headers for reconnect)."""
196200
if self._task is not None and not self._task.done():
197201
if self._stop is not None:
198202
self._stop.set()

mtv_agent/lib/mcp/mcp_manager.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Manages multiple named MCP SSE connections with tool-name routing."""
1+
"""Manages multiple named MCP connections with tool-name routing."""
22

33
from __future__ import annotations
44

@@ -16,7 +16,7 @@
1616

1717

1818
class MCPManager:
19-
"""Manages multiple named MCP SSE connections with tool-name routing.
19+
"""Manages multiple named MCP connections with tool-name routing.
2020
2121
Keeps a registry of all *configured* servers (from the config file) so
2222
that clients can list available servers and connect/disconnect by name
@@ -34,7 +34,7 @@ def load_configs(self, servers: dict[str, MCPServerConfig]) -> None:
3434
self._configs.update(servers)
3535

3636
async def connect_all(self, servers: dict[str, MCPServerConfig]) -> None:
37-
"""Store configs and open SSE connections to every server.
37+
"""Store configs and open connections to every server.
3838
3939
Servers that are unreachable are logged and skipped so the
4040
application can still start. They remain in ``_configs`` and
@@ -128,7 +128,7 @@ def get_server_info(self) -> list[dict]:
128128
]
129129

130130
async def disconnect_all(self) -> None:
131-
"""Tear down every SSE connection (in parallel)."""
131+
"""Tear down every MCP connection (in parallel)."""
132132
if self._clients:
133133
results = await asyncio.gather(
134134
*(c.disconnect() for c in self._clients.values()),

mtv_agent/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ async def lifespan(app: FastAPI):
9595
api_url, token = resolve_kube_credentials()
9696
if api_url and token:
9797
inject_kube_headers(mcp_servers, api_url, token)
98-
logger.info("Injected kube auth headers for MCP SSE connections")
98+
logger.info("Injected kube auth headers for MCP connections")
9999
elif api_url or token:
100100
inject_kube_headers(mcp_servers, api_url, token)
101101
logger.warning(

0 commit comments

Comments
 (0)