diff --git a/docs/src/content/docs/agents/mcp-tool-provider.mdx b/docs/src/content/docs/agents/mcp-tool-provider.mdx
index eb583d05..816ddbfd 100644
--- a/docs/src/content/docs/agents/mcp-tool-provider.mdx
+++ b/docs/src/content/docs/agents/mcp-tool-provider.mdx
@@ -85,7 +85,7 @@ await provider.disconnect()
## Transports
-`MCPToolProvider` supports two transports.
+`MCPToolProvider` supports three transports: `stdio`, `streamable-http`, and `sse`.
### stdio — spawn a local process
@@ -122,9 +122,44 @@ provider = await MCPToolProvider.create([
-### SSE — connect to a remote server
+### Streamable HTTP — connect to a remote server
-Connect to an MCP server running over HTTP Server-Sent Events (SSE).
+The current standard HTTP transport for remote MCP servers. Use this for any server you don't spawn locally, unless it only speaks the older SSE protocol.
+
+
+
+```typescript
+import { MCPToolProvider } from "agent-squad";
+
+const provider = await MCPToolProvider.create([
+ {
+ type: "streamable-http",
+ url: "http://localhost:3000/mcp",
+ headers: { Authorization: `Bearer ${process.env.MCP_TOKEN}` },
+ },
+]);
+```
+Requires `@modelcontextprotocol/sdk` >= 1.10.
+
+
+```python
+from agent_squad.tools import MCPToolProvider, MCPServerConfig
+
+provider = await MCPToolProvider.create([
+ MCPServerConfig(
+ type="streamable-http",
+ url="http://localhost:3000/mcp",
+ headers={"Authorization": f"Bearer {os.environ['MCP_TOKEN']}"},
+ )
+])
+```
+Requires `mcp` >= 1.9 (already satisfied by a fresh `pip install "agent-squad[mcp]"`).
+
+
+
+### SSE — connect to a legacy remote server
+
+Connect to an MCP server running over HTTP Server-Sent Events (SSE). This is the older HTTP transport, deprecated by the MCP spec since 2025-03-26 — prefer `streamable-http` for servers that support it.
@@ -167,7 +202,7 @@ import { MCPToolProvider } from "agent-squad";
const provider = await MCPToolProvider.create([
{ type: "stdio", command: "uvx", args: ["filesystem-server"] },
{ type: "stdio", command: "uvx", args: ["database-server"] },
- { type: "sse", url: "https://api.example.com/mcp" },
+ { type: "streamable-http", url: "https://api.example.com/mcp" },
]);
```
@@ -178,7 +213,7 @@ from agent_squad.tools import MCPToolProvider, MCPServerConfig
provider = await MCPToolProvider.create([
MCPServerConfig(type="stdio", command="uvx", args=["filesystem-server"]),
MCPServerConfig(type="stdio", command="uvx", args=["database-server"]),
- MCPServerConfig(type="sse", url="https://api.example.com/mcp"),
+ MCPServerConfig(type="streamable-http", url="https://api.example.com/mcp"),
])
```
@@ -259,22 +294,22 @@ Available in the Python and TypeScript `MCPToolProvider`. See the [GroundedAgent
| Field | Type | Required | Description |
|---|---|---|---|
-| `type` | `"stdio" \| "sse"` | Yes | Transport type |
+| `type` | `"stdio" \| "streamable-http" \| "sse"` | Yes | Transport type |
| `command` | `string` | stdio only | Executable to launch |
| `args` | `string[]` | No | Arguments for the command |
| `env` | `Record` | No | Environment variables for the subprocess |
-| `url` | `string` | sse only | Full URL of the SSE endpoint |
-| `headers` | `Record` | No | HTTP headers for the SSE connection |
+| `url` | `string` | streamable-http / sse | Full URL of the server endpoint |
+| `headers` | `Record` | No | HTTP headers for the connection |
| Field | Type | Required | Description |
|---|---|---|---|
-| `type` | `str` (`"stdio"` or `"sse"`) | Yes | Transport type |
+| `type` | `str` (`"stdio"`, `"streamable-http"` or `"sse"`) | Yes | Transport type |
| `command` | `str` | stdio only | Executable to launch |
| `args` | `list[str]` | No | Arguments for the command |
| `env` | `dict[str, str]` | No | Environment variables for the subprocess |
-| `url` | `str` | sse only | Full URL of the SSE endpoint |
-| `headers` | `dict[str, str]` | No | HTTP headers for the SSE connection |
+| `url` | `str` | streamable-http / sse | Full URL of the server endpoint |
+| `headers` | `dict[str, str]` | No | HTTP headers for the connection |
diff --git a/python/src/agent_squad/tools/mcp_tool_provider.py b/python/src/agent_squad/tools/mcp_tool_provider.py
index 8921ac84..57be6f9b 100644
--- a/python/src/agent_squad/tools/mcp_tool_provider.py
+++ b/python/src/agent_squad/tools/mcp_tool_provider.py
@@ -10,6 +10,7 @@
provider = await MCPToolProvider.create([
MCPServerConfig(type="stdio", command="uvx", args=["my-mcp-server"]),
+ MCPServerConfig(type="streamable-http", url="http://localhost:3000/mcp"),
MCPServerConfig(type="sse", url="http://localhost:3000/sse"),
])
@@ -42,6 +43,13 @@
"Install it with: pip install agent-squad[mcp]"
) from exc
+# Guarded separately: the module only exists since mcp ~1.9, and older installs
+# must keep working as long as they don't use the streamable-http transport.
+try:
+ from mcp.client.streamable_http import streamablehttp_client
+except ImportError:
+ streamablehttp_client = None
+
from pydantic import AnyUrl # mcp depends on pydantic, so it's available whenever the import above succeeds
from dataclasses import dataclass, field
@@ -57,18 +65,20 @@ class MCPServerConfig:
"""Configuration for a single MCP server.
For stdio transport set ``type="stdio"`` and provide ``command`` / ``args`` / ``env``.
- For SSE/HTTP transport set ``type="sse"`` and provide ``url`` / ``headers``.
+ For Streamable HTTP (the current standard HTTP transport) set
+ ``type="streamable-http"`` and provide ``url`` / ``headers``.
+ For the legacy HTTP+SSE transport set ``type="sse"`` and provide ``url`` / ``headers``.
Attributes:
- type: Transport type — ``"stdio"`` or ``"sse"``.
+ type: Transport type — ``"stdio"``, ``"streamable-http"`` or ``"sse"``.
command: Executable to launch (stdio only).
args: Command-line arguments (stdio only).
env: Environment variables to pass to the subprocess (stdio only).
- url: SSE endpoint URL (sse only).
- headers: HTTP headers to send with the SSE connection (sse only).
+ url: Server endpoint URL (streamable-http / sse).
+ headers: HTTP headers to send with the connection (streamable-http / sse).
"""
- type: str # "stdio" or "sse"
+ type: str # "stdio", "streamable-http" or "sse"
command: Optional[str] = None
args: list[str] = field(default_factory=list)
env: Optional[dict[str, str]] = None
@@ -128,12 +138,12 @@ class MCPToolProvider(AgentTools):
provider = await MCPToolProvider.create([
MCPServerConfig(type="stdio", command="uvx", args=["my-server"]),
- MCPServerConfig(type="sse", url="http://localhost:3000/sse"),
+ MCPServerConfig(type="streamable-http", url="http://localhost:3000/mcp"),
])
tool_config={"tool": provider}
Call :meth:`disconnect` when the provider is no longer needed to cleanly
- shut down stdio child processes or SSE connections.
+ shut down stdio child processes or HTTP connections.
Args:
servers: List of :class:`MCPServerConfig` describing the MCP servers to
@@ -207,13 +217,23 @@ async def _ensure_connected(self) -> None:
if not server_cfg.url:
raise ValueError("MCPServerConfig with type='sse' requires a 'url'")
cm = sse_client(server_cfg.url, headers=server_cfg.headers or {})
+ elif server_cfg.type == "streamable-http":
+ if streamablehttp_client is None:
+ raise ImportError(
+ "The streamable-http transport requires mcp>=1.9. "
+ "Upgrade it with: pip install -U 'mcp>=1.9,<2'"
+ )
+ if not server_cfg.url:
+ raise ValueError("MCPServerConfig with type='streamable-http' requires a 'url'")
+ cm = streamablehttp_client(server_cfg.url, headers=server_cfg.headers or {})
else:
raise ValueError(
f"Unsupported MCPServerConfig type: '{server_cfg.type}'. "
- "Use 'stdio' or 'sse'."
+ "Use 'stdio', 'streamable-http' or 'sse'."
)
- read, write = await cm.__aenter__()
+ # stdio/sse yield (read, write); streamable-http yields (read, write, get_session_id)
+ read, write, *_ = await cm.__aenter__()
self._cm_stack.append(cm)
session = ClientSession(read, write)
diff --git a/python/src/tests/tools/test_mcp_tool_provider.py b/python/src/tests/tools/test_mcp_tool_provider.py
index 8bb10c00..e3759dda 100644
--- a/python/src/tests/tools/test_mcp_tool_provider.py
+++ b/python/src/tests/tools/test_mcp_tool_provider.py
@@ -61,18 +61,21 @@ def mock_mcp_modules():
mock_client_session_cls = MagicMock()
mock_stdio_client = MagicMock()
mock_sse_client = MagicMock()
+ mock_streamablehttp_client = MagicMock()
mock_stdio_params_cls = MagicMock()
with (
patch("agent_squad.tools.mcp_tool_provider.ClientSession", mock_client_session_cls),
patch("agent_squad.tools.mcp_tool_provider.stdio_client", mock_stdio_client),
patch("agent_squad.tools.mcp_tool_provider.sse_client", mock_sse_client),
+ patch("agent_squad.tools.mcp_tool_provider.streamablehttp_client", mock_streamablehttp_client),
patch("agent_squad.tools.mcp_tool_provider.StdioServerParameters", mock_stdio_params_cls),
):
yield {
"ClientSession": mock_client_session_cls,
"stdio_client": mock_stdio_client,
"sse_client": mock_sse_client,
+ "streamablehttp_client": mock_streamablehttp_client,
"StdioServerParameters": mock_stdio_params_cls,
}
@@ -404,6 +407,72 @@ async def test_lazy_connection_sse(mock_mcp_modules):
)
+@pytest.mark.asyncio
+async def test_lazy_connection_streamable_http(mock_mcp_modules):
+ from agent_squad.tools.mcp_tool_provider import MCPToolProvider, MCPServerConfig
+
+ tool = _make_mcp_tool("search", "Search")
+
+ read_mock = MagicMock()
+ write_mock = MagicMock()
+ get_session_id_mock = MagicMock()
+ fake_cm = AsyncMock()
+ # streamablehttp_client yields a 3-tuple, unlike stdio/sse
+ fake_cm.__aenter__ = AsyncMock(return_value=(read_mock, write_mock, get_session_id_mock))
+ fake_cm.__aexit__ = AsyncMock(return_value=False)
+
+ mock_mcp_modules["streamablehttp_client"].return_value = fake_cm
+
+ mock_session_instance = AsyncMock()
+ mock_session_instance.__aenter__ = AsyncMock(return_value=mock_session_instance)
+ mock_session_instance.__aexit__ = AsyncMock(return_value=False)
+ mock_session_instance.initialize = AsyncMock()
+ mock_session_instance.list_tools = AsyncMock(
+ return_value=_make_list_tools_result([tool])
+ )
+ mock_mcp_modules["ClientSession"].return_value = mock_session_instance
+
+ provider = MCPToolProvider(
+ [MCPServerConfig(
+ type="streamable-http",
+ url="http://localhost:9000/mcp",
+ headers={"x-api-key": "abc"},
+ )]
+ )
+
+ await provider._ensure_connected()
+
+ assert provider._connected
+ assert "search" in provider._tool_map
+ mock_mcp_modules["streamablehttp_client"].assert_called_once_with(
+ "http://localhost:9000/mcp", headers={"x-api-key": "abc"}
+ )
+
+
+@pytest.mark.asyncio
+async def test_streamable_http_missing_url(mock_mcp_modules):
+ from agent_squad.tools.mcp_tool_provider import MCPToolProvider, MCPServerConfig
+
+ provider = MCPToolProvider([MCPServerConfig(type="streamable-http")]) # no url
+
+ with pytest.raises(ValueError, match="url"):
+ await provider._ensure_connected()
+
+
+@pytest.mark.asyncio
+async def test_streamable_http_requires_recent_mcp(mock_mcp_modules):
+ """When the installed mcp predates the streamable_http module, the error must say how to fix it."""
+ from agent_squad.tools.mcp_tool_provider import MCPToolProvider, MCPServerConfig
+
+ provider = MCPToolProvider(
+ [MCPServerConfig(type="streamable-http", url="http://localhost:9000/mcp")]
+ )
+
+ with patch("agent_squad.tools.mcp_tool_provider.streamablehttp_client", None):
+ with pytest.raises(ImportError, match="mcp>=1.9"):
+ await provider._ensure_connected()
+
+
@pytest.mark.asyncio
async def test_ensure_connected_idempotent(mock_mcp_modules):
"""Calling _ensure_connected twice should not reconnect."""
diff --git a/typescript/src/tools/mcpToolProvider.ts b/typescript/src/tools/mcpToolProvider.ts
index cc1bf98c..db96d012 100644
--- a/typescript/src/tools/mcpToolProvider.ts
+++ b/typescript/src/tools/mcpToolProvider.ts
@@ -21,17 +21,21 @@ function modelVisible(meta: any): boolean {
* Configuration for a single MCP server connection.
*/
export interface MCPServerConfig {
- /** Transport type: stdio (spawn a local process) or sse (HTTP SSE endpoint) */
- type: "stdio" | "sse";
+ /**
+ * Transport type: stdio (spawn a local process), streamable-http (the current
+ * standard HTTP transport), or sse (the legacy HTTP+SSE transport, kept for
+ * older servers).
+ */
+ type: "stdio" | "sse" | "streamable-http";
/** stdio only: command to execute */
command?: string;
/** stdio only: arguments for the command */
args?: string[];
/** stdio only: environment variables for the child process */
env?: Record;
- /** sse only: full URL of the SSE endpoint */
+ /** streamable-http / sse: full URL of the server endpoint */
url?: string;
- /** sse only: extra HTTP headers */
+ /** streamable-http / sse: extra HTTP headers */
headers?: Record;
}
@@ -47,6 +51,7 @@ export interface MCPServerConfig {
* ```typescript
* const provider = await MCPToolProvider.create([
* { type: "stdio", command: "uvx", args: ["my-mcp-server"] },
+ * { type: "streamable-http", url: "http://localhost:3000/mcp" },
* { type: "sse", url: "http://localhost:3000/sse" },
* ]);
*
@@ -145,6 +150,15 @@ export class MCPToolProvider extends AgentTools {
SSEClientTransport = null;
}
+ let StreamableHTTPClientTransport: any;
+ try {
+ // @ts-ignore — optional peerDependency; module exists since SDK ~1.10
+ const streamableMod = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
+ StreamableHTTPClientTransport = streamableMod.StreamableHTTPClientTransport;
+ } catch {
+ StreamableHTTPClientTransport = null;
+ }
+
const allTools: AgentTool[] = [];
for (const serverConfig of this.servers) {
@@ -180,9 +194,26 @@ export class MCPToolProvider extends AgentTools {
transport = new SSEClientTransport(new URL(serverConfig.url), {
headers: serverConfig.headers ?? {},
});
+ } else if (serverConfig.type === "streamable-http") {
+ if (!StreamableHTTPClientTransport) {
+ throw new Error(
+ "StreamableHTTPClientTransport not available — upgrade @modelcontextprotocol/sdk (requires >=1.10)"
+ );
+ }
+ if (!serverConfig.url) {
+ throw new Error(
+ "MCPServerConfig with type 'streamable-http' requires a 'url' field"
+ );
+ }
+ transport = new StreamableHTTPClientTransport(
+ new URL(serverConfig.url),
+ serverConfig.headers
+ ? { requestInit: { headers: serverConfig.headers } }
+ : undefined
+ );
} else {
throw new Error(
- `Unsupported MCPServerConfig type: ${(serverConfig as any).type}`
+ `Unsupported MCPServerConfig type: ${(serverConfig as any).type}. Use 'stdio', 'streamable-http', or 'sse'.`
);
}
diff --git a/typescript/tests/mcpToolProvider.test.ts b/typescript/tests/mcpToolProvider.test.ts
index 22c34836..d36b9aa0 100644
--- a/typescript/tests/mcpToolProvider.test.ts
+++ b/typescript/tests/mcpToolProvider.test.ts
@@ -30,9 +30,17 @@ class MockSSETransport {
constructor(public url: any, public opts: any) {}
}
+const streamableHttpInstances: MockStreamableHTTPTransport[] = [];
+class MockStreamableHTTPTransport {
+ constructor(public url: any, public opts: any) {
+ streamableHttpInstances.push(this);
+ }
+}
+
jest.mock("@modelcontextprotocol/sdk/client/index.js", () => ({ Client: MockClient }), { virtual: true });
jest.mock("@modelcontextprotocol/sdk/client/stdio.js", () => ({ StdioClientTransport: MockStdioTransport }), { virtual: true });
jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({ SSEClientTransport: MockSSETransport }), { virtual: true });
+jest.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ StreamableHTTPClientTransport: MockStreamableHTTPTransport }), { virtual: true });
// ---------------------------------------------------------------------------
// Subject under test
@@ -431,6 +439,46 @@ describe("MCPToolProvider", () => {
expect(mockConnect).toHaveBeenCalled();
});
+ // -------------------------------------------------------------------------
+ // Streamable HTTP transport
+ // -------------------------------------------------------------------------
+
+ it("creates StreamableHTTPClientTransport with headers in requestInit", async () => {
+ streamableHttpInstances.length = 0;
+ const provider = new MCPToolProvider([
+ { type: "streamable-http", url: "http://localhost:9000/mcp", headers: { "x-api-key": "abc" } },
+ ]);
+
+ await provider.ensureConnected();
+ expect(mockConnect).toHaveBeenCalled();
+ expect(streamableHttpInstances).toHaveLength(1);
+ expect(streamableHttpInstances[0].url.href).toBe("http://localhost:9000/mcp");
+ expect(streamableHttpInstances[0].opts).toEqual({
+ requestInit: { headers: { "x-api-key": "abc" } },
+ });
+ });
+
+ it("creates StreamableHTTPClientTransport without options when no headers given", async () => {
+ streamableHttpInstances.length = 0;
+ const provider = new MCPToolProvider([
+ { type: "streamable-http", url: "http://localhost:9000/mcp" },
+ ]);
+
+ await provider.ensureConnected();
+ expect(streamableHttpInstances).toHaveLength(1);
+ expect(streamableHttpInstances[0].opts).toBeUndefined();
+ });
+
+ it("throws when streamable-http server config has no url", async () => {
+ const badProvider = new MCPToolProvider([
+ { type: "streamable-http" } as MCPServerConfig,
+ ]);
+
+ await expect(badProvider.ensureConnected()).rejects.toThrow(
+ "requires a 'url' field"
+ );
+ });
+
// -------------------------------------------------------------------------
// Config validation
// -------------------------------------------------------------------------