diff --git a/docs/src/content/docs/agents/mcp-tool-provider.mdx b/docs/src/content/docs/agents/mcp-tool-provider.mdx
index 816ddbfd..aec3dc22 100644
--- a/docs/src/content/docs/agents/mcp-tool-provider.mdx
+++ b/docs/src/content/docs/agents/mcp-tool-provider.mdx
@@ -22,9 +22,18 @@ The async factory pattern is required because the agent needs tool definitions s
```bash
npm install agent-squad
+npm install @modelcontextprotocol/client # MCP SDK v2 — recommended
+```
+Or the v1 SDK, if you only talk to servers on protocol 2025-11-25 or older:
+```bash
npm install @modelcontextprotocol/sdk
```
-`@modelcontextprotocol/sdk` is a peer dependency — it is never installed automatically, only when you explicitly add it.
+Both are optional peer dependencies — never installed automatically. When both are present, `MCPToolProvider` uses v2.
+
+| Installed package | MCP protocol versions | Notes |
+|---|---|---|
+| `@modelcontextprotocol/client` >= 2.0.0 | 2026-07-28 **and** all legacy versions | auto-negotiated per server (`server/discover` probe with `initialize` fallback); Node >= 20 |
+| `@modelcontextprotocol/sdk` >= 1.0.0 | 2025-11-25 and older | the v1 package will never support 2026-07-28 |
```bash
@@ -139,7 +148,7 @@ const provider = await MCPToolProvider.create([
},
]);
```
-Requires `@modelcontextprotocol/sdk` >= 1.10.
+Requires `@modelcontextprotocol/client` (any version) or `@modelcontextprotocol/sdk` >= 1.10.
```python
diff --git a/typescript/package.json b/typescript/package.json
index d76bee85..73099c7d 100644
--- a/typescript/package.json
+++ b/typescript/package.json
@@ -48,12 +48,16 @@
},
"peerDependencies": {
"@dakera-ai/dakera": "^0.11.100",
+ "@modelcontextprotocol/client": ">=2.0.0",
"@modelcontextprotocol/sdk": ">=1.0.0"
},
"peerDependenciesMeta": {
"@dakera-ai/dakera": {
"optional": true
},
+ "@modelcontextprotocol/client": {
+ "optional": true
+ },
"@modelcontextprotocol/sdk": {
"optional": true
}
diff --git a/typescript/src/tools/mcpToolProvider.ts b/typescript/src/tools/mcpToolProvider.ts
index db96d012..8830e15f 100644
--- a/typescript/src/tools/mcpToolProvider.ts
+++ b/typescript/src/tools/mcpToolProvider.ts
@@ -65,7 +65,13 @@ export interface MCPServerConfig {
* await provider.disconnect();
* ```
*
- * The `@modelcontextprotocol/sdk` package must be installed separately:
+ * An MCP SDK must be installed separately — either the v2 client package
+ * (speaks protocol 2026-07-28 and every older server via auto negotiation;
+ * preferred when both are installed):
+ * ```
+ * npm install @modelcontextprotocol/client
+ * ```
+ * or the v1 SDK (older servers only):
* ```
* npm install @modelcontextprotocol/sdk
* ```
@@ -123,40 +129,64 @@ export class MCPToolProvider extends AgentTools {
let ClientClass: any;
let StdioClientTransport: any;
let SSEClientTransport: any;
+ let StreamableHTTPClientTransport: any;
+ // v2 (@modelcontextprotocol/client) speaks both the 2026-07-28 stateless
+ // protocol and the legacy handshake; preferred over v1 when installed.
+ let usingV2 = false;
try {
// @ts-ignore — optional peerDependency; not available in type-checking until installed
- const clientMod = await import("@modelcontextprotocol/sdk/client/index.js");
- ClientClass = clientMod.Client;
+ const v2Mod = await import("@modelcontextprotocol/client");
+ ClientClass = v2Mod.Client;
+ SSEClientTransport = v2Mod.SSEClientTransport;
+ StreamableHTTPClientTransport = v2Mod.StreamableHTTPClientTransport;
+ usingV2 = true;
+ try {
+ // @ts-ignore — optional peerDependency; Node-only subpath
+ const v2StdioMod = await import("@modelcontextprotocol/client/stdio");
+ StdioClientTransport = v2StdioMod.StdioClientTransport;
+ } catch {
+ StdioClientTransport = null;
+ }
} catch {
- throw new Error(
- "Install @modelcontextprotocol/sdk to use MCPToolProvider: npm install @modelcontextprotocol/sdk"
- );
+ usingV2 = false;
}
- try {
- // @ts-ignore — optional peerDependency
- const stdioMod = await import("@modelcontextprotocol/sdk/client/stdio.js");
- StdioClientTransport = stdioMod.StdioClientTransport;
- } catch {
- StdioClientTransport = null;
- }
+ if (!usingV2) {
+ try {
+ // @ts-ignore — optional peerDependency
+ const clientMod = await import("@modelcontextprotocol/sdk/client/index.js");
+ ClientClass = clientMod.Client;
+ } catch {
+ throw new Error(
+ "Install an MCP SDK to use MCPToolProvider: npm install @modelcontextprotocol/client " +
+ "(supports protocol 2026-07-28 and older servers) or npm install @modelcontextprotocol/sdk (older servers only)"
+ );
+ }
- try {
- // @ts-ignore — optional peerDependency
- const sseMod = await import("@modelcontextprotocol/sdk/client/sse.js");
- SSEClientTransport = sseMod.SSEClientTransport;
- } catch {
- SSEClientTransport = null;
- }
+ try {
+ // @ts-ignore — optional peerDependency
+ const stdioMod = await import("@modelcontextprotocol/sdk/client/stdio.js");
+ StdioClientTransport = stdioMod.StdioClientTransport;
+ } catch {
+ StdioClientTransport = 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;
+ try {
+ // @ts-ignore — optional peerDependency
+ const sseMod = await import("@modelcontextprotocol/sdk/client/sse.js");
+ SSEClientTransport = sseMod.SSEClientTransport;
+ } catch {
+ SSEClientTransport = null;
+ }
+
+ 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[] = [];
@@ -167,7 +197,8 @@ export class MCPToolProvider extends AgentTools {
if (serverConfig.type === "stdio") {
if (!StdioClientTransport) {
throw new Error(
- "StdioClientTransport not available — check your @modelcontextprotocol/sdk installation"
+ "StdioClientTransport not available — check your MCP SDK installation " +
+ `(${usingV2 ? "@modelcontextprotocol/client" : "@modelcontextprotocol/sdk"})`
);
}
if (!serverConfig.command) {
@@ -183,7 +214,8 @@ export class MCPToolProvider extends AgentTools {
} else if (serverConfig.type === "sse") {
if (!SSEClientTransport) {
throw new Error(
- "SSEClientTransport not available — check your @modelcontextprotocol/sdk installation"
+ "SSEClientTransport not available — check your MCP SDK installation " +
+ `(${usingV2 ? "@modelcontextprotocol/client" : "@modelcontextprotocol/sdk"})`
);
}
if (!serverConfig.url) {
@@ -191,13 +223,24 @@ export class MCPToolProvider extends AgentTools {
"MCPServerConfig with type 'sse' requires a 'url' field"
);
}
- transport = new SSEClientTransport(new URL(serverConfig.url), {
- headers: serverConfig.headers ?? {},
- });
+ // v2 has no top-level `headers` option; POST-channel headers go via
+ // requestInit (GET-stream headers tracked in #640).
+ transport = usingV2
+ ? new SSEClientTransport(
+ new URL(serverConfig.url),
+ serverConfig.headers
+ ? { requestInit: { headers: serverConfig.headers } }
+ : undefined
+ )
+ : 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)"
+ usingV2
+ ? "StreamableHTTPClientTransport not available — check your @modelcontextprotocol/client installation"
+ : "StreamableHTTPClientTransport not available — upgrade @modelcontextprotocol/sdk (requires >=1.10)"
);
}
if (!serverConfig.url) {
@@ -217,10 +260,17 @@ export class MCPToolProvider extends AgentTools {
);
}
- const client = new ClientClass(
- { name: "agent-squad-mcp-client", version: "1.0.0" },
- { capabilities: {} }
- );
+ // mode "auto" probes server/discover (protocol 2026-07-28) and falls back
+ // to the legacy initialize handshake, so both server eras work.
+ const client = usingV2
+ ? new ClientClass(
+ { name: "agent-squad-mcp-client", version: "1.0.0" },
+ { capabilities: {}, versionNegotiation: { mode: "auto" } }
+ )
+ : new ClientClass(
+ { name: "agent-squad-mcp-client", version: "1.0.0" },
+ { capabilities: {} }
+ );
await client.connect(transport);
this.clients.push(client);
diff --git a/typescript/tests/mcpToolProviderV2.test.ts b/typescript/tests/mcpToolProviderV2.test.ts
new file mode 100644
index 00000000..f9762b3d
--- /dev/null
+++ b/typescript/tests/mcpToolProviderV2.test.ts
@@ -0,0 +1,234 @@
+/**
+ * Unit tests for MCPToolProvider on the v2 MCP SDK (@modelcontextprotocol/client).
+ *
+ * The v2 package is virtually mocked; when present it must be preferred over v1
+ * and the client must be constructed with versionNegotiation mode "auto" so both
+ * 2026-07-28 and legacy servers work. The v1 suite (mcpToolProvider.test.ts) does
+ * NOT mock the v2 package, which proves the v1 fallback path stays intact.
+ */
+
+const mockCallTool = jest.fn();
+const mockListTools = jest.fn();
+const mockConnect = jest.fn();
+const mockClose = jest.fn();
+const mockReadResource = jest.fn();
+
+const clientInstances: MockV2Client[] = [];
+class MockV2Client {
+ connect = mockConnect;
+ listTools = mockListTools;
+ callTool = mockCallTool;
+ close = mockClose;
+ readResource = mockReadResource;
+ constructor(public info: any, public options: any) {
+ clientInstances.push(this);
+ }
+}
+
+class MockV2StdioTransport {
+ constructor(public opts: any) {}
+}
+
+const sseInstances: MockV2SSETransport[] = [];
+class MockV2SSETransport {
+ constructor(public url: any, public opts: any) {
+ sseInstances.push(this);
+ }
+}
+
+const streamableInstances: MockV2StreamableTransport[] = [];
+class MockV2StreamableTransport {
+ constructor(public url: any, public opts: any) {
+ streamableInstances.push(this);
+ }
+}
+
+// v1 mock: a sentinel that must never be reached while v2 is installed
+const v1ClientConstructed = jest.fn();
+class MockV1Client {
+ constructor() {
+ v1ClientConstructed();
+ }
+}
+
+jest.mock(
+ "@modelcontextprotocol/client",
+ () => ({
+ Client: MockV2Client,
+ SSEClientTransport: MockV2SSETransport,
+ StreamableHTTPClientTransport: MockV2StreamableTransport,
+ }),
+ { virtual: true }
+);
+jest.mock(
+ "@modelcontextprotocol/client/stdio",
+ () => ({ StdioClientTransport: MockV2StdioTransport }),
+ { virtual: true }
+);
+jest.mock(
+ "@modelcontextprotocol/sdk/client/index.js",
+ () => ({ Client: MockV1Client }),
+ { virtual: true }
+);
+
+import { MCPToolProvider } from "../src/tools/mcpToolProvider";
+
+const weatherTool = {
+ name: "get_weather",
+ description: "Returns weather for a location",
+ inputSchema: {
+ type: "object",
+ properties: { location: { type: "string" } },
+ required: ["location"],
+ },
+};
+
+beforeEach(() => {
+ jest.clearAllMocks();
+ clientInstances.length = 0;
+ sseInstances.length = 0;
+ streamableInstances.length = 0;
+ mockListTools.mockResolvedValue({ tools: [weatherTool] });
+ mockConnect.mockResolvedValue(undefined);
+});
+
+describe("MCPToolProvider on the v2 SDK", () => {
+ it("prefers v2 over v1 and never constructs the v1 client", async () => {
+ const provider = new MCPToolProvider([
+ { type: "stdio", command: "uvx", args: ["my-server"] },
+ ]);
+
+ await provider.ensureConnected();
+
+ expect(clientInstances).toHaveLength(1);
+ expect(v1ClientConstructed).not.toHaveBeenCalled();
+ });
+
+ it("constructs the v2 client with versionNegotiation mode auto", async () => {
+ const provider = new MCPToolProvider([
+ { type: "stdio", command: "uvx", args: ["my-server"] },
+ ]);
+
+ await provider.ensureConnected();
+
+ expect(clientInstances[0].info).toEqual({
+ name: "agent-squad-mcp-client",
+ version: "1.0.0",
+ });
+ expect(clientInstances[0].options).toEqual({
+ capabilities: {},
+ versionNegotiation: { mode: "auto" },
+ });
+ });
+
+ it("uses the v2 stdio transport from the /stdio subpath", async () => {
+ const provider = new MCPToolProvider([
+ {
+ type: "stdio",
+ command: "uvx",
+ args: ["my-server"],
+ env: { API_KEY: "k" },
+ },
+ ]);
+
+ await provider.ensureConnected();
+
+ expect(mockConnect).toHaveBeenCalledWith(expect.any(MockV2StdioTransport));
+ const transport = mockConnect.mock.calls[0][0] as MockV2StdioTransport;
+ expect(transport.opts).toEqual({
+ command: "uvx",
+ args: ["my-server"],
+ env: { API_KEY: "k" },
+ });
+ });
+
+ it("constructs the v2 streamable-http transport with requestInit headers", async () => {
+ const provider = new MCPToolProvider([
+ {
+ type: "streamable-http",
+ url: "http://localhost:9000/mcp",
+ headers: { "x-api-key": "abc" },
+ },
+ ]);
+
+ await provider.ensureConnected();
+
+ expect(streamableInstances).toHaveLength(1);
+ expect(streamableInstances[0].url.href).toBe("http://localhost:9000/mcp");
+ expect(streamableInstances[0].opts).toEqual({
+ requestInit: { headers: { "x-api-key": "abc" } },
+ });
+ });
+
+ it("constructs the v2 sse transport with requestInit headers (no dead headers key)", async () => {
+ const provider = new MCPToolProvider([
+ {
+ type: "sse",
+ url: "http://localhost:9000/sse",
+ headers: { "x-api-key": "abc" },
+ },
+ ]);
+
+ await provider.ensureConnected();
+
+ expect(sseInstances).toHaveLength(1);
+ expect(sseInstances[0].opts).toEqual({
+ requestInit: { headers: { "x-api-key": "abc" } },
+ });
+ });
+
+ it("passes no options to the v2 sse transport when no headers are configured", async () => {
+ const provider = new MCPToolProvider([
+ { type: "sse", url: "http://localhost:9000/sse" },
+ ]);
+
+ await provider.ensureConnected();
+
+ expect(sseInstances).toHaveLength(1);
+ expect(sseInstances[0].opts).toBeUndefined();
+ });
+
+ it("lists and calls tools through the v2 client", async () => {
+ mockCallTool.mockResolvedValue({
+ content: [{ type: "text", text: "sunny" }],
+ isError: false,
+ });
+
+ const provider = await MCPToolProvider.create([
+ { type: "stdio", command: "uvx", args: ["my-server"] },
+ ]);
+
+ const formats = await provider.toBedrockFormat();
+ expect(formats).toHaveLength(1);
+ expect(formats[0].toolSpec.name).toBe("get_weather");
+
+ const response = {
+ role: "assistant",
+ content: [
+ { toolUse: { name: "get_weather", toolUseId: "1", input: { location: "Paris" } } },
+ ],
+ };
+ await provider.toolHandler(
+ response,
+ (b: any) => b.toolUse ?? null,
+ (b: any) => b.name,
+ (b: any) => b.toolUseId,
+ (b: any) => b.input
+ );
+
+ expect(mockCallTool).toHaveBeenCalledWith({
+ name: "get_weather",
+ arguments: { location: "Paris" },
+ });
+ });
+
+ it("disconnect closes the v2 client", async () => {
+ const provider = await MCPToolProvider.create([
+ { type: "stdio", command: "uvx", args: ["my-server"] },
+ ]);
+
+ await provider.disconnect();
+
+ expect(mockClose).toHaveBeenCalled();
+ });
+});
diff --git a/typescript/tests/mcpToolProviderV2Partial.test.ts b/typescript/tests/mcpToolProviderV2Partial.test.ts
new file mode 100644
index 00000000..fdc132ec
--- /dev/null
+++ b/typescript/tests/mcpToolProviderV2Partial.test.ts
@@ -0,0 +1,49 @@
+/**
+ * Edge case: the v2 root package resolves but its Node-only /stdio subpath does
+ * not (e.g. a bundled/browser-ish environment). stdio configs must fail with an
+ * error naming the v2 package, and HTTP transports must keep working.
+ */
+
+class MockV2Client {
+ connect = jest.fn().mockResolvedValue(undefined);
+ listTools = jest.fn().mockResolvedValue({ tools: [] });
+ close = jest.fn();
+}
+
+class MockV2StreamableTransport {
+ constructor(public url: any, public opts: any) {}
+}
+
+// Only the root subpath is mocked — "@modelcontextprotocol/client/stdio" stays
+// unresolvable, so StdioClientTransport ends up null while usingV2 is true.
+jest.mock(
+ "@modelcontextprotocol/client",
+ () => ({
+ Client: MockV2Client,
+ SSEClientTransport: class {},
+ StreamableHTTPClientTransport: MockV2StreamableTransport,
+ }),
+ { virtual: true }
+);
+
+import { MCPToolProvider } from "../src/tools/mcpToolProvider";
+
+describe("MCPToolProvider with v2 root but no /stdio subpath", () => {
+ it("fails stdio configs with an error naming @modelcontextprotocol/client", async () => {
+ const provider = new MCPToolProvider([
+ { type: "stdio", command: "uvx", args: ["my-server"] },
+ ]);
+
+ await expect(provider.ensureConnected()).rejects.toThrow(
+ "@modelcontextprotocol/client"
+ );
+ });
+
+ it("still connects streamable-http configs", async () => {
+ const provider = new MCPToolProvider([
+ { type: "streamable-http", url: "http://localhost:9000/mcp" },
+ ]);
+
+ await provider.ensureConnected();
+ });
+});