@tambo-ai/react 0.53.2 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/__tests__/use-mcp-servers.test.d.ts +2 -0
- package/dist/mcp/__tests__/use-mcp-servers.test.d.ts.map +1 -0
- package/dist/mcp/__tests__/use-mcp-servers.test.js +118 -0
- package/dist/mcp/__tests__/use-mcp-servers.test.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +2 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/tambo-mcp-provider.d.ts +34 -2
- package/dist/mcp/tambo-mcp-provider.d.ts.map +1 -1
- package/dist/mcp/tambo-mcp-provider.js +108 -14
- package/dist/mcp/tambo-mcp-provider.js.map +1 -1
- package/esm/mcp/__tests__/use-mcp-servers.test.d.ts +2 -0
- package/esm/mcp/__tests__/use-mcp-servers.test.d.ts.map +1 -0
- package/esm/mcp/__tests__/use-mcp-servers.test.js +83 -0
- package/esm/mcp/__tests__/use-mcp-servers.test.js.map +1 -0
- package/esm/mcp/index.d.ts +1 -1
- package/esm/mcp/index.d.ts.map +1 -1
- package/esm/mcp/index.js +1 -1
- package/esm/mcp/index.js.map +1 -1
- package/esm/mcp/tambo-mcp-provider.d.ts +34 -2
- package/esm/mcp/tambo-mcp-provider.d.ts.map +1 -1
- package/esm/mcp/tambo-mcp-provider.js +73 -13
- package/esm/mcp/tambo-mcp-provider.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-mcp-servers.test.d.ts","sourceRoot":"","sources":["../../../src/mcp/__tests__/use-mcp-servers.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const react_1 = require("@testing-library/react");
|
|
37
|
+
const react_2 = __importStar(require("react"));
|
|
38
|
+
const tambo_registry_provider_1 = require("../../providers/tambo-registry-provider");
|
|
39
|
+
const tambo_mcp_provider_1 = require("../tambo-mcp-provider");
|
|
40
|
+
// Mock the registry to provide a no-op registerTool
|
|
41
|
+
// Do not mock the registry; use the real provider in render
|
|
42
|
+
// Mock the MCP client; use a mutable implementation to avoid TDZ issues
|
|
43
|
+
let createImpl = jest.fn();
|
|
44
|
+
jest.mock("../mcp-client", () => ({
|
|
45
|
+
MCPClient: { create: (...args) => createImpl(...args) },
|
|
46
|
+
MCPTransport: { SSE: "sse", HTTP: "http" },
|
|
47
|
+
}));
|
|
48
|
+
// Import after mocks note: jest.mock calls are hoisted, so standard imports are fine
|
|
49
|
+
describe("useTamboMcpServers + TamboMcpProvider", () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
createImpl = jest.fn();
|
|
52
|
+
});
|
|
53
|
+
it("provides normalized MCP server entries to inner components", async () => {
|
|
54
|
+
const fakeClient = { listTools: jest.fn().mockResolvedValue([]) };
|
|
55
|
+
createImpl.mockResolvedValue(fakeClient);
|
|
56
|
+
const Inner = () => {
|
|
57
|
+
const servers = (0, tambo_mcp_provider_1.useTamboMcpServers)();
|
|
58
|
+
return (react_2.default.createElement("div", null,
|
|
59
|
+
react_2.default.createElement("div", { "data-testid": "count" }, servers.length),
|
|
60
|
+
react_2.default.createElement("div", { "data-testid": "urls" }, servers.map((s) => s.url).join(","))));
|
|
61
|
+
};
|
|
62
|
+
const { getByTestId } = (0, react_1.render)(react_2.default.createElement(tambo_registry_provider_1.TamboRegistryProvider, null,
|
|
63
|
+
react_2.default.createElement(tambo_mcp_provider_1.TamboMcpProvider, { mcpServers: [{ url: "https://one.example" }, "https://two.example"] },
|
|
64
|
+
react_2.default.createElement(Inner, null))));
|
|
65
|
+
await (0, react_1.waitFor)(() => {
|
|
66
|
+
expect(getByTestId("count").textContent).toBe("2");
|
|
67
|
+
const urls = getByTestId("urls").textContent || "";
|
|
68
|
+
expect(urls).toContain("https://one.example");
|
|
69
|
+
expect(urls).toContain("https://two.example");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
it("marks a successfully connected server with a client instance", async () => {
|
|
73
|
+
const fakeClient = { listTools: jest.fn().mockResolvedValue([]) };
|
|
74
|
+
createImpl.mockResolvedValue(fakeClient);
|
|
75
|
+
let latest = [];
|
|
76
|
+
const Capture = () => {
|
|
77
|
+
const servers = (0, tambo_mcp_provider_1.useTamboMcpServers)();
|
|
78
|
+
(0, react_2.useEffect)(() => {
|
|
79
|
+
latest = servers;
|
|
80
|
+
}, [servers]);
|
|
81
|
+
return null;
|
|
82
|
+
};
|
|
83
|
+
(0, react_1.render)(react_2.default.createElement(tambo_registry_provider_1.TamboRegistryProvider, null,
|
|
84
|
+
react_2.default.createElement(tambo_mcp_provider_1.TamboMcpProvider, { mcpServers: [{ url: "https://ok.example" }] },
|
|
85
|
+
react_2.default.createElement(Capture, null))));
|
|
86
|
+
await (0, react_1.waitFor)(() => {
|
|
87
|
+
expect(latest.length).toBe(1);
|
|
88
|
+
expect("client" in latest[0]).toBe(true);
|
|
89
|
+
expect(latest[0].client).toBe(fakeClient);
|
|
90
|
+
// no connectionError on connected server
|
|
91
|
+
expect(latest[0].connectionError).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
it("marks a failed server with a connectionError and no client", async () => {
|
|
95
|
+
const boom = new Error("boom");
|
|
96
|
+
createImpl.mockRejectedValue(boom);
|
|
97
|
+
let latest = [];
|
|
98
|
+
const Capture = () => {
|
|
99
|
+
const servers = (0, tambo_mcp_provider_1.useTamboMcpServers)();
|
|
100
|
+
(0, react_2.useEffect)(() => {
|
|
101
|
+
latest = servers;
|
|
102
|
+
}, [servers]);
|
|
103
|
+
return null;
|
|
104
|
+
};
|
|
105
|
+
(0, react_1.render)(react_2.default.createElement(tambo_registry_provider_1.TamboRegistryProvider, null,
|
|
106
|
+
react_2.default.createElement(tambo_mcp_provider_1.TamboMcpProvider, { mcpServers: [{ url: "https://fail.example" }] },
|
|
107
|
+
react_2.default.createElement(Capture, null))));
|
|
108
|
+
await (0, react_1.waitFor)(() => {
|
|
109
|
+
expect(latest.length).toBe(1);
|
|
110
|
+
expect("client" in latest[0]).toBe(false);
|
|
111
|
+
// @ts-expect-error narrowing at runtime
|
|
112
|
+
expect(latest[0].connectionError).toBeInstanceOf(Error);
|
|
113
|
+
// @ts-expect-error narrowing at runtime
|
|
114
|
+
expect(latest[0].connectionError?.message).toBe("boom");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
//# sourceMappingURL=use-mcp-servers.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-mcp-servers.test.js","sourceRoot":"","sources":["../../../src/mcp/__tests__/use-mcp-servers.test.tsx"],"names":[],"mappings":"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","sourcesContent":["import { render, waitFor } from \"@testing-library/react\";\nimport React, { useEffect } from \"react\";\nimport { TamboRegistryProvider } from \"../../providers/tambo-registry-provider\";\nimport {\n TamboMcpProvider,\n useTamboMcpServers,\n type McpServer,\n} from \"../tambo-mcp-provider\";\n\n// Mock the registry to provide a no-op registerTool\n// Do not mock the registry; use the real provider in render\n\n// Mock the MCP client; use a mutable implementation to avoid TDZ issues\nlet createImpl: jest.Mock<any, any> = jest.fn();\njest.mock(\"../mcp-client\", () => ({\n MCPClient: { create: (...args: any[]) => createImpl(...args) },\n MCPTransport: { SSE: \"sse\", HTTP: \"http\" },\n}));\n\n// Import after mocks note: jest.mock calls are hoisted, so standard imports are fine\n\ndescribe(\"useTamboMcpServers + TamboMcpProvider\", () => {\n beforeEach(() => {\n createImpl = jest.fn();\n });\n\n it(\"provides normalized MCP server entries to inner components\", async () => {\n const fakeClient = { listTools: jest.fn().mockResolvedValue([]) } as any;\n createImpl.mockResolvedValue(fakeClient);\n\n const Inner: React.FC = () => {\n const servers = useTamboMcpServers();\n return (\n <div>\n <div data-testid=\"count\">{servers.length}</div>\n <div data-testid=\"urls\">{servers.map((s) => s.url).join(\",\")}</div>\n </div>\n );\n };\n\n const { getByTestId } = render(\n <TamboRegistryProvider>\n <TamboMcpProvider\n mcpServers={[{ url: \"https://one.example\" }, \"https://two.example\"]}\n >\n <Inner />\n </TamboMcpProvider>\n </TamboRegistryProvider>,\n );\n\n await waitFor(() => {\n expect(getByTestId(\"count\").textContent).toBe(\"2\");\n const urls = getByTestId(\"urls\").textContent || \"\";\n expect(urls).toContain(\"https://one.example\");\n expect(urls).toContain(\"https://two.example\");\n });\n });\n\n it(\"marks a successfully connected server with a client instance\", async () => {\n const fakeClient = { listTools: jest.fn().mockResolvedValue([]) } as any;\n createImpl.mockResolvedValue(fakeClient);\n\n let latest: McpServer[] = [];\n const Capture: React.FC = () => {\n const servers = useTamboMcpServers();\n useEffect(() => {\n latest = servers;\n }, [servers]);\n return null;\n };\n\n render(\n <TamboRegistryProvider>\n <TamboMcpProvider mcpServers={[{ url: \"https://ok.example\" }]}>\n <Capture />\n </TamboMcpProvider>\n </TamboRegistryProvider>,\n );\n\n await waitFor(() => {\n expect(latest.length).toBe(1);\n expect(\"client\" in latest[0]).toBe(true);\n expect((latest[0] as any).client).toBe(fakeClient);\n // no connectionError on connected server\n expect((latest[0] as any).connectionError).toBeUndefined();\n });\n });\n\n it(\"marks a failed server with a connectionError and no client\", async () => {\n const boom = new Error(\"boom\");\n createImpl.mockRejectedValue(boom);\n\n let latest: McpServer[] = [];\n const Capture: React.FC = () => {\n const servers = useTamboMcpServers();\n useEffect(() => {\n latest = servers;\n }, [servers]);\n return null;\n };\n\n render(\n <TamboRegistryProvider>\n <TamboMcpProvider mcpServers={[{ url: \"https://fail.example\" }]}>\n <Capture />\n </TamboMcpProvider>\n </TamboRegistryProvider>,\n );\n\n await waitFor(() => {\n expect(latest.length).toBe(1);\n expect(\"client\" in latest[0]).toBe(false);\n // @ts-expect-error narrowing at runtime\n expect(latest[0].connectionError).toBeInstanceOf(Error);\n // @ts-expect-error narrowing at runtime\n expect(latest[0].connectionError?.message).toBe(\"boom\");\n });\n });\n});\n"]}
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { MCPTransport } from "./mcp-client";
|
|
2
|
-
export { TamboMcpProvider, type McpServerInfo } from "./tambo-mcp-provider";
|
|
2
|
+
export { TamboMcpProvider, useTamboMcpServers, type ConnectedMcpServer, type FailedMcpServer, type McpServer, type McpServerInfo, } from "./tambo-mcp-provider";
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/mcp/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,aAAa,GACnB,MAAM,sBAAsB,CAAC"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TamboMcpProvider = exports.MCPTransport = void 0;
|
|
3
|
+
exports.useTamboMcpServers = exports.TamboMcpProvider = exports.MCPTransport = void 0;
|
|
4
4
|
var mcp_client_1 = require("./mcp-client");
|
|
5
5
|
Object.defineProperty(exports, "MCPTransport", { enumerable: true, get: function () { return mcp_client_1.MCPTransport; } });
|
|
6
6
|
var tambo_mcp_provider_1 = require("./tambo-mcp-provider");
|
|
7
7
|
Object.defineProperty(exports, "TamboMcpProvider", { enumerable: true, get: function () { return tambo_mcp_provider_1.TamboMcpProvider; } });
|
|
8
|
+
Object.defineProperty(exports, "useTamboMcpServers", { enumerable: true, get: function () { return tambo_mcp_provider_1.useTamboMcpServers; } });
|
|
8
9
|
//# sourceMappingURL=index.js.map
|
package/dist/mcp/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":";;;AAAA,2CAA4C;AAAnC,0GAAA,YAAY,OAAA;AACrB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":";;;AAAA,2CAA4C;AAAnC,0GAAA,YAAY,OAAA;AACrB,2DAO8B;AAN5B,sHAAA,gBAAgB,OAAA;AAChB,wHAAA,kBAAkB,OAAA","sourcesContent":["export { MCPTransport } from \"./mcp-client\";\nexport {\n TamboMcpProvider,\n useTamboMcpServers,\n type ConnectedMcpServer,\n type FailedMcpServer,\n type McpServer,\n type McpServerInfo,\n} from \"./tambo-mcp-provider\";\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { FC } from "react";
|
|
2
|
-
import { MCPTransport } from "./mcp-client";
|
|
1
|
+
import React, { FC } from "react";
|
|
2
|
+
import { MCPClient, MCPTransport } from "./mcp-client";
|
|
3
3
|
/**
|
|
4
4
|
* Extracts error message from MCP tool result content.
|
|
5
5
|
* Handles both array and string content formats.
|
|
@@ -14,6 +14,14 @@ export interface McpServerInfo {
|
|
|
14
14
|
transport?: MCPTransport;
|
|
15
15
|
customHeaders?: Record<string, string>;
|
|
16
16
|
}
|
|
17
|
+
export interface ConnectedMcpServer extends McpServerInfo {
|
|
18
|
+
client: MCPClient;
|
|
19
|
+
}
|
|
20
|
+
export interface FailedMcpServer extends McpServerInfo {
|
|
21
|
+
client?: never;
|
|
22
|
+
connectionError: Error;
|
|
23
|
+
}
|
|
24
|
+
export type McpServer = ConnectedMcpServer | FailedMcpServer;
|
|
17
25
|
/**
|
|
18
26
|
* This provider is used to register tools from MCP servers.
|
|
19
27
|
* @returns the wrapped children
|
|
@@ -22,4 +30,28 @@ export declare const TamboMcpProvider: FC<{
|
|
|
22
30
|
mcpServers: (McpServerInfo | string)[];
|
|
23
31
|
children: React.ReactNode;
|
|
24
32
|
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Hook to access the actual MCP servers, as they are connected (or fail to
|
|
35
|
+
* connect).
|
|
36
|
+
*
|
|
37
|
+
* You can call methods on the MCP client that is included in the MCP server
|
|
38
|
+
* object.
|
|
39
|
+
*
|
|
40
|
+
* If the server fails to connect, the `client` property will be `undefined` and
|
|
41
|
+
* the `connectionError` property will be set.
|
|
42
|
+
*
|
|
43
|
+
* For example, to forcibly disconnect and reconnect all MCP servers:
|
|
44
|
+
*
|
|
45
|
+
* ```tsx
|
|
46
|
+
* const mcpServers = useMcpServers();
|
|
47
|
+
* mcpServers.forEach((mcpServer) => {
|
|
48
|
+
* mcpServer.client?.reconnect();
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* Note that the MCP servers are not guaranteed to be in the same order as the
|
|
53
|
+
* input array, because they are added as they are connected.
|
|
54
|
+
* @returns The MCP servers
|
|
55
|
+
*/
|
|
56
|
+
export declare const useTamboMcpServers: () => McpServer[];
|
|
25
57
|
//# sourceMappingURL=tambo-mcp-provider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tambo-mcp-provider.d.ts","sourceRoot":"","sources":["../../src/mcp/tambo-mcp-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,
|
|
1
|
+
{"version":3,"file":"tambo-mcp-provider.d.ts","sourceRoot":"","sources":["../../src/mcp/tambo-mcp-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAEZ,EAAE,EAIH,MAAM,OAAO,CAAC;AAGf,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEvD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAsB5D;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,kBAAmB,SAAQ,aAAa;IACvD,MAAM,EAAE,SAAS,CAAC;CACnB;AAED,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,eAAe,EAAE,KAAK,CAAC;CACxB;AAED,MAAM,MAAM,SAAS,GAAG,kBAAkB,GAAG,eAAe,CAAC;AAG7D;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,EAAE,CAAC;IAChC,UAAU,EAAE,CAAC,aAAa,GAAG,MAAM,CAAC,EAAE,CAAC;IACvC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAsHA,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kBAAkB,mBAE9B,CAAC"}
|
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TamboMcpProvider = void 0;
|
|
36
|
+
exports.useTamboMcpServers = exports.TamboMcpProvider = void 0;
|
|
4
37
|
exports.extractErrorMessage = extractErrorMessage;
|
|
5
|
-
const react_1 = require("react");
|
|
38
|
+
const react_1 = __importStar(require("react"));
|
|
6
39
|
const tambo_registry_provider_1 = require("../providers/tambo-registry-provider");
|
|
7
40
|
const mcp_client_1 = require("./mcp-client");
|
|
8
41
|
/**
|
|
@@ -28,28 +61,54 @@ function extractErrorMessage(content) {
|
|
|
28
61
|
}
|
|
29
62
|
return `${content}`;
|
|
30
63
|
}
|
|
64
|
+
const McpProviderContext = (0, react_1.createContext)([]);
|
|
31
65
|
/**
|
|
32
66
|
* This provider is used to register tools from MCP servers.
|
|
33
67
|
* @returns the wrapped children
|
|
34
68
|
*/
|
|
35
69
|
const TamboMcpProvider = ({ mcpServers, children }) => {
|
|
36
70
|
const { registerTool } = (0, tambo_registry_provider_1.useTamboRegistry)();
|
|
71
|
+
const [connectedMcpServers, setConnectedMcpServers] = (0, react_1.useState)([]);
|
|
37
72
|
(0, react_1.useEffect)(() => {
|
|
38
73
|
if (!mcpServers) {
|
|
39
74
|
return;
|
|
40
75
|
}
|
|
41
|
-
async function registerMcpServers(
|
|
76
|
+
async function registerMcpServers(mcpServerInfos) {
|
|
42
77
|
// Maps tool names to the MCP client that registered them
|
|
43
78
|
const mcpServerMap = new Map();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
79
|
+
// initialize the MCP clients, converting McpServerInfo -> McpServer
|
|
80
|
+
const mcpServers = await Promise.allSettled(mcpServerInfos.map(async (mcpServerInfo) => {
|
|
81
|
+
try {
|
|
82
|
+
const client = await mcp_client_1.MCPClient.create(mcpServerInfo.url, mcpServerInfo.transport, mcpServerInfo.customHeaders);
|
|
83
|
+
const connectedMcpServer = {
|
|
84
|
+
...mcpServerInfo,
|
|
85
|
+
client: client,
|
|
86
|
+
};
|
|
87
|
+
// note because the promises may resolve in any order, the resulting
|
|
88
|
+
// array may not be in the same order as the input array
|
|
89
|
+
setConnectedMcpServers((prev) => [...prev, connectedMcpServer]);
|
|
90
|
+
return connectedMcpServer;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const failedMcpServer = {
|
|
94
|
+
...mcpServerInfo,
|
|
95
|
+
connectionError: error,
|
|
96
|
+
};
|
|
97
|
+
// note because the promises may resolve in any order, the resulting
|
|
98
|
+
// array may not be in the same order as the input array
|
|
99
|
+
setConnectedMcpServers((prev) => [...prev, failedMcpServer]);
|
|
100
|
+
return failedMcpServer;
|
|
101
|
+
}
|
|
102
|
+
}));
|
|
103
|
+
// note do not rely on the state
|
|
104
|
+
const connectedMcpServers = mcpServers
|
|
105
|
+
.filter((result) => result.status === "fulfilled")
|
|
106
|
+
.map((result) => result.value);
|
|
107
|
+
// Now create a map of tool name to MCP client
|
|
108
|
+
const serverToolLists = connectedMcpServers.map(async (mcpServer) => {
|
|
109
|
+
const tools = (await mcpServer.client?.listTools()) ?? [];
|
|
51
110
|
tools.forEach((tool) => {
|
|
52
|
-
mcpServerMap.set(tool.name,
|
|
111
|
+
mcpServerMap.set(tool.name, mcpServer);
|
|
53
112
|
});
|
|
54
113
|
return tools;
|
|
55
114
|
});
|
|
@@ -74,7 +133,11 @@ const TamboMcpProvider = ({ mcpServers, children }) => {
|
|
|
74
133
|
// should never happen
|
|
75
134
|
throw new Error(`MCP server for tool ${tool.name} not found`);
|
|
76
135
|
}
|
|
77
|
-
|
|
136
|
+
if (!mcpServer.client) {
|
|
137
|
+
// this can't actually happen because the tool can't be registered if the server is not connected
|
|
138
|
+
throw new Error(`MCP server for tool ${tool.name} is not connected`);
|
|
139
|
+
}
|
|
140
|
+
const result = await mcpServer.client.callTool(tool.name, args);
|
|
78
141
|
if (result.isError) {
|
|
79
142
|
const errorMessage = extractErrorMessage(result.content);
|
|
80
143
|
throw new Error(errorMessage);
|
|
@@ -85,9 +148,40 @@ const TamboMcpProvider = ({ mcpServers, children }) => {
|
|
|
85
148
|
});
|
|
86
149
|
});
|
|
87
150
|
}
|
|
88
|
-
|
|
151
|
+
// normalize the server infos
|
|
152
|
+
const mcpServerInfos = mcpServers.map((mcpServer) => typeof mcpServer === "string"
|
|
153
|
+
? { url: mcpServer, transport: mcp_client_1.MCPTransport.SSE }
|
|
154
|
+
: mcpServer);
|
|
155
|
+
registerMcpServers(mcpServerInfos);
|
|
89
156
|
}, [mcpServers, registerTool]);
|
|
90
|
-
return children;
|
|
157
|
+
return (react_1.default.createElement(McpProviderContext.Provider, { value: connectedMcpServers }, children));
|
|
91
158
|
};
|
|
92
159
|
exports.TamboMcpProvider = TamboMcpProvider;
|
|
160
|
+
/**
|
|
161
|
+
* Hook to access the actual MCP servers, as they are connected (or fail to
|
|
162
|
+
* connect).
|
|
163
|
+
*
|
|
164
|
+
* You can call methods on the MCP client that is included in the MCP server
|
|
165
|
+
* object.
|
|
166
|
+
*
|
|
167
|
+
* If the server fails to connect, the `client` property will be `undefined` and
|
|
168
|
+
* the `connectionError` property will be set.
|
|
169
|
+
*
|
|
170
|
+
* For example, to forcibly disconnect and reconnect all MCP servers:
|
|
171
|
+
*
|
|
172
|
+
* ```tsx
|
|
173
|
+
* const mcpServers = useMcpServers();
|
|
174
|
+
* mcpServers.forEach((mcpServer) => {
|
|
175
|
+
* mcpServer.client?.reconnect();
|
|
176
|
+
* });
|
|
177
|
+
* ```
|
|
178
|
+
*
|
|
179
|
+
* Note that the MCP servers are not guaranteed to be in the same order as the
|
|
180
|
+
* input array, because they are added as they are connected.
|
|
181
|
+
* @returns The MCP servers
|
|
182
|
+
*/
|
|
183
|
+
const useTamboMcpServers = () => {
|
|
184
|
+
return (0, react_1.useContext)(McpProviderContext);
|
|
185
|
+
};
|
|
186
|
+
exports.useTamboMcpServers = useTamboMcpServers;
|
|
93
187
|
//# sourceMappingURL=tambo-mcp-provider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tambo-mcp-provider.js","sourceRoot":"","sources":["../../src/mcp/tambo-mcp-provider.tsx"],"names":[],"mappings":"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","sourcesContent":["import { FC, useEffect } from \"react\";\nimport { TamboTool } from \"../model/component-metadata\";\nimport { useTamboRegistry } from \"../providers/tambo-registry-provider\";\nimport { MCPClient, MCPTransport } from \"./mcp-client\";\n\n/**\n * Extracts error message from MCP tool result content.\n * Handles both array and string content formats.\n * Always returns a string, even for invalid/null inputs.\n * @returns The extracted error message as a string\n */\nexport function extractErrorMessage(content: unknown): string {\n if (content === undefined || content === null) {\n return \"Unknown error occurred\";\n }\n\n if (Array.isArray(content)) {\n const textItems = content\n .filter(\n (item) => item && item.type === \"text\" && typeof item.text === \"string\",\n )\n .map((item) => item.text);\n\n return textItems.length > 0\n ? textItems.join(\" \")\n : \"Error occurred but no details provided\";\n }\n\n if (typeof content === \"object\") {\n return JSON.stringify(content);\n }\n\n return `${content}`;\n}\n\nexport interface McpServerInfo {\n name?: string;\n url: string;\n description?: string;\n transport?: MCPTransport;\n customHeaders?: Record<string, string>;\n}\n/**\n * This provider is used to register tools from MCP servers.\n * @returns the wrapped children\n */\nexport const TamboMcpProvider: FC<{\n mcpServers: (McpServerInfo | string)[];\n children: React.ReactNode;\n}> = ({ mcpServers, children }) => {\n const { registerTool } = useTamboRegistry();\n\n useEffect(() => {\n if (!mcpServers) {\n return;\n }\n async function registerMcpServers(mcpServers: (McpServerInfo | string)[]) {\n // Maps tool names to the MCP client that registered them\n const mcpServerMap = new Map<string, MCPClient>();\n const serverToolLists = mcpServers.map(async (mcpServer) => {\n const server =\n typeof mcpServer === \"string\"\n ? { url: mcpServer, transport: MCPTransport.SSE }\n : mcpServer;\n const { url, transport = MCPTransport.SSE, customHeaders } = server;\n const mcpClient = await MCPClient.create(url, transport, customHeaders);\n const tools = await mcpClient.listTools();\n tools.forEach((tool) => {\n mcpServerMap.set(tool.name, mcpClient);\n });\n return tools;\n });\n const toolResults = await Promise.allSettled(serverToolLists);\n\n // Just log the failed tools, we can't do anything about them\n const failedTools = toolResults.filter(\n (result) => result.status === \"rejected\",\n );\n if (failedTools.length > 0) {\n console.error(\n \"Failed to register tools from MCP servers:\",\n failedTools.map((result) => result.reason),\n );\n }\n\n // Register the successful tools\n const allTools = toolResults\n .filter((result) => result.status === \"fulfilled\")\n .map((result) => result.value)\n .flat();\n allTools.forEach((tool) => {\n registerTool({\n description: tool.description ?? \"\",\n name: tool.name,\n tool: async (args: Record<string, unknown>) => {\n const mcpServer = mcpServerMap.get(tool.name);\n if (!mcpServer) {\n // should never happen\n throw new Error(`MCP server for tool ${tool.name} not found`);\n }\n const result = await mcpServer.callTool(tool.name, args);\n if (result.isError) {\n const errorMessage = extractErrorMessage(result.content);\n throw new Error(errorMessage);\n }\n return result.content;\n },\n toolSchema: tool.inputSchema as TamboTool[\"toolSchema\"],\n });\n });\n }\n registerMcpServers(mcpServers);\n }, [mcpServers, registerTool]);\n\n return children;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"tambo-mcp-provider.js","sourceRoot":"","sources":["../../src/mcp/tambo-mcp-provider.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, {\n createContext,\n FC,\n useContext,\n useEffect,\n useState,\n} from \"react\";\nimport { TamboTool } from \"../model/component-metadata\";\nimport { useTamboRegistry } from \"../providers/tambo-registry-provider\";\nimport { MCPClient, MCPTransport } from \"./mcp-client\";\n\n/**\n * Extracts error message from MCP tool result content.\n * Handles both array and string content formats.\n * Always returns a string, even for invalid/null inputs.\n * @returns The extracted error message as a string\n */\nexport function extractErrorMessage(content: unknown): string {\n if (content === undefined || content === null) {\n return \"Unknown error occurred\";\n }\n\n if (Array.isArray(content)) {\n const textItems = content\n .filter(\n (item) => item && item.type === \"text\" && typeof item.text === \"string\",\n )\n .map((item) => item.text);\n\n return textItems.length > 0\n ? textItems.join(\" \")\n : \"Error occurred but no details provided\";\n }\n\n if (typeof content === \"object\") {\n return JSON.stringify(content);\n }\n\n return `${content}`;\n}\n\nexport interface McpServerInfo {\n name?: string;\n url: string;\n description?: string;\n transport?: MCPTransport;\n customHeaders?: Record<string, string>;\n}\n\nexport interface ConnectedMcpServer extends McpServerInfo {\n client: MCPClient;\n}\n\nexport interface FailedMcpServer extends McpServerInfo {\n client?: never;\n connectionError: Error;\n}\n\nexport type McpServer = ConnectedMcpServer | FailedMcpServer;\n\nconst McpProviderContext = createContext<McpServer[]>([]);\n/**\n * This provider is used to register tools from MCP servers.\n * @returns the wrapped children\n */\nexport const TamboMcpProvider: FC<{\n mcpServers: (McpServerInfo | string)[];\n children: React.ReactNode;\n}> = ({ mcpServers, children }) => {\n const { registerTool } = useTamboRegistry();\n const [connectedMcpServers, setConnectedMcpServers] = useState<McpServer[]>(\n [],\n );\n\n useEffect(() => {\n if (!mcpServers) {\n return;\n }\n async function registerMcpServers(mcpServerInfos: McpServerInfo[]) {\n // Maps tool names to the MCP client that registered them\n const mcpServerMap = new Map<string, McpServer>();\n\n // initialize the MCP clients, converting McpServerInfo -> McpServer\n const mcpServers = await Promise.allSettled(\n mcpServerInfos.map(async (mcpServerInfo): Promise<McpServer> => {\n try {\n const client = await MCPClient.create(\n mcpServerInfo.url,\n mcpServerInfo.transport,\n mcpServerInfo.customHeaders,\n );\n const connectedMcpServer = {\n ...mcpServerInfo,\n client: client,\n };\n // note because the promises may resolve in any order, the resulting\n // array may not be in the same order as the input array\n setConnectedMcpServers((prev) => [...prev, connectedMcpServer]);\n return connectedMcpServer;\n } catch (error) {\n const failedMcpServer = {\n ...mcpServerInfo,\n connectionError: error as Error,\n };\n // note because the promises may resolve in any order, the resulting\n // array may not be in the same order as the input array\n setConnectedMcpServers((prev) => [...prev, failedMcpServer]);\n return failedMcpServer;\n }\n }),\n );\n\n // note do not rely on the state\n const connectedMcpServers = mcpServers\n .filter((result) => result.status === \"fulfilled\")\n .map((result) => result.value);\n\n // Now create a map of tool name to MCP client\n const serverToolLists = connectedMcpServers.map(async (mcpServer) => {\n const tools = (await mcpServer.client?.listTools()) ?? [];\n tools.forEach((tool) => {\n mcpServerMap.set(tool.name, mcpServer);\n });\n return tools;\n });\n const toolResults = await Promise.allSettled(serverToolLists);\n\n // Just log the failed tools, we can't do anything about them\n const failedTools = toolResults.filter(\n (result) => result.status === \"rejected\",\n );\n if (failedTools.length > 0) {\n console.error(\n \"Failed to register tools from MCP servers:\",\n failedTools.map((result) => result.reason),\n );\n }\n\n // Register the successful tools\n const allTools = toolResults\n .filter((result) => result.status === \"fulfilled\")\n .map((result) => result.value)\n .flat();\n allTools.forEach((tool) => {\n registerTool({\n description: tool.description ?? \"\",\n name: tool.name,\n tool: async (args: Record<string, unknown>) => {\n const mcpServer = mcpServerMap.get(tool.name);\n if (!mcpServer) {\n // should never happen\n throw new Error(`MCP server for tool ${tool.name} not found`);\n }\n if (!mcpServer.client) {\n // this can't actually happen because the tool can't be registered if the server is not connected\n throw new Error(\n `MCP server for tool ${tool.name} is not connected`,\n );\n }\n const result = await mcpServer.client.callTool(tool.name, args);\n if (result.isError) {\n const errorMessage = extractErrorMessage(result.content);\n throw new Error(errorMessage);\n }\n return result.content;\n },\n toolSchema: tool.inputSchema as TamboTool[\"toolSchema\"],\n });\n });\n }\n\n // normalize the server infos\n const mcpServerInfos = mcpServers.map((mcpServer) =>\n typeof mcpServer === \"string\"\n ? { url: mcpServer, transport: MCPTransport.SSE }\n : mcpServer,\n );\n\n registerMcpServers(mcpServerInfos);\n }, [mcpServers, registerTool]);\n\n return (\n <McpProviderContext.Provider value={connectedMcpServers}>\n {children}\n </McpProviderContext.Provider>\n );\n};\n\n/**\n * Hook to access the actual MCP servers, as they are connected (or fail to\n * connect).\n *\n * You can call methods on the MCP client that is included in the MCP server\n * object.\n *\n * If the server fails to connect, the `client` property will be `undefined` and\n * the `connectionError` property will be set.\n *\n * For example, to forcibly disconnect and reconnect all MCP servers:\n *\n * ```tsx\n * const mcpServers = useMcpServers();\n * mcpServers.forEach((mcpServer) => {\n * mcpServer.client?.reconnect();\n * });\n * ```\n *\n * Note that the MCP servers are not guaranteed to be in the same order as the\n * input array, because they are added as they are connected.\n * @returns The MCP servers\n */\nexport const useTamboMcpServers = () => {\n return useContext(McpProviderContext);\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-mcp-servers.test.d.ts","sourceRoot":"","sources":["../../../src/mcp/__tests__/use-mcp-servers.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { render, waitFor } from "@testing-library/react";
|
|
2
|
+
import React, { useEffect } from "react";
|
|
3
|
+
import { TamboRegistryProvider } from "../../providers/tambo-registry-provider";
|
|
4
|
+
import { TamboMcpProvider, useTamboMcpServers, } from "../tambo-mcp-provider";
|
|
5
|
+
// Mock the registry to provide a no-op registerTool
|
|
6
|
+
// Do not mock the registry; use the real provider in render
|
|
7
|
+
// Mock the MCP client; use a mutable implementation to avoid TDZ issues
|
|
8
|
+
let createImpl = jest.fn();
|
|
9
|
+
jest.mock("../mcp-client", () => ({
|
|
10
|
+
MCPClient: { create: (...args) => createImpl(...args) },
|
|
11
|
+
MCPTransport: { SSE: "sse", HTTP: "http" },
|
|
12
|
+
}));
|
|
13
|
+
// Import after mocks note: jest.mock calls are hoisted, so standard imports are fine
|
|
14
|
+
describe("useTamboMcpServers + TamboMcpProvider", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
createImpl = jest.fn();
|
|
17
|
+
});
|
|
18
|
+
it("provides normalized MCP server entries to inner components", async () => {
|
|
19
|
+
const fakeClient = { listTools: jest.fn().mockResolvedValue([]) };
|
|
20
|
+
createImpl.mockResolvedValue(fakeClient);
|
|
21
|
+
const Inner = () => {
|
|
22
|
+
const servers = useTamboMcpServers();
|
|
23
|
+
return (React.createElement("div", null,
|
|
24
|
+
React.createElement("div", { "data-testid": "count" }, servers.length),
|
|
25
|
+
React.createElement("div", { "data-testid": "urls" }, servers.map((s) => s.url).join(","))));
|
|
26
|
+
};
|
|
27
|
+
const { getByTestId } = render(React.createElement(TamboRegistryProvider, null,
|
|
28
|
+
React.createElement(TamboMcpProvider, { mcpServers: [{ url: "https://one.example" }, "https://two.example"] },
|
|
29
|
+
React.createElement(Inner, null))));
|
|
30
|
+
await waitFor(() => {
|
|
31
|
+
expect(getByTestId("count").textContent).toBe("2");
|
|
32
|
+
const urls = getByTestId("urls").textContent || "";
|
|
33
|
+
expect(urls).toContain("https://one.example");
|
|
34
|
+
expect(urls).toContain("https://two.example");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
it("marks a successfully connected server with a client instance", async () => {
|
|
38
|
+
const fakeClient = { listTools: jest.fn().mockResolvedValue([]) };
|
|
39
|
+
createImpl.mockResolvedValue(fakeClient);
|
|
40
|
+
let latest = [];
|
|
41
|
+
const Capture = () => {
|
|
42
|
+
const servers = useTamboMcpServers();
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
latest = servers;
|
|
45
|
+
}, [servers]);
|
|
46
|
+
return null;
|
|
47
|
+
};
|
|
48
|
+
render(React.createElement(TamboRegistryProvider, null,
|
|
49
|
+
React.createElement(TamboMcpProvider, { mcpServers: [{ url: "https://ok.example" }] },
|
|
50
|
+
React.createElement(Capture, null))));
|
|
51
|
+
await waitFor(() => {
|
|
52
|
+
expect(latest.length).toBe(1);
|
|
53
|
+
expect("client" in latest[0]).toBe(true);
|
|
54
|
+
expect(latest[0].client).toBe(fakeClient);
|
|
55
|
+
// no connectionError on connected server
|
|
56
|
+
expect(latest[0].connectionError).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
it("marks a failed server with a connectionError and no client", async () => {
|
|
60
|
+
const boom = new Error("boom");
|
|
61
|
+
createImpl.mockRejectedValue(boom);
|
|
62
|
+
let latest = [];
|
|
63
|
+
const Capture = () => {
|
|
64
|
+
const servers = useTamboMcpServers();
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
latest = servers;
|
|
67
|
+
}, [servers]);
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
render(React.createElement(TamboRegistryProvider, null,
|
|
71
|
+
React.createElement(TamboMcpProvider, { mcpServers: [{ url: "https://fail.example" }] },
|
|
72
|
+
React.createElement(Capture, null))));
|
|
73
|
+
await waitFor(() => {
|
|
74
|
+
expect(latest.length).toBe(1);
|
|
75
|
+
expect("client" in latest[0]).toBe(false);
|
|
76
|
+
// @ts-expect-error narrowing at runtime
|
|
77
|
+
expect(latest[0].connectionError).toBeInstanceOf(Error);
|
|
78
|
+
// @ts-expect-error narrowing at runtime
|
|
79
|
+
expect(latest[0].connectionError?.message).toBe("boom");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=use-mcp-servers.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-mcp-servers.test.js","sourceRoot":"","sources":["../../../src/mcp/__tests__/use-mcp-servers.test.tsx"],"names":[],"mappings":"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","sourcesContent":["import { render, waitFor } from \"@testing-library/react\";\nimport React, { useEffect } from \"react\";\nimport { TamboRegistryProvider } from \"../../providers/tambo-registry-provider\";\nimport {\n TamboMcpProvider,\n useTamboMcpServers,\n type McpServer,\n} from \"../tambo-mcp-provider\";\n\n// Mock the registry to provide a no-op registerTool\n// Do not mock the registry; use the real provider in render\n\n// Mock the MCP client; use a mutable implementation to avoid TDZ issues\nlet createImpl: jest.Mock<any, any> = jest.fn();\njest.mock(\"../mcp-client\", () => ({\n MCPClient: { create: (...args: any[]) => createImpl(...args) },\n MCPTransport: { SSE: \"sse\", HTTP: \"http\" },\n}));\n\n// Import after mocks note: jest.mock calls are hoisted, so standard imports are fine\n\ndescribe(\"useTamboMcpServers + TamboMcpProvider\", () => {\n beforeEach(() => {\n createImpl = jest.fn();\n });\n\n it(\"provides normalized MCP server entries to inner components\", async () => {\n const fakeClient = { listTools: jest.fn().mockResolvedValue([]) } as any;\n createImpl.mockResolvedValue(fakeClient);\n\n const Inner: React.FC = () => {\n const servers = useTamboMcpServers();\n return (\n <div>\n <div data-testid=\"count\">{servers.length}</div>\n <div data-testid=\"urls\">{servers.map((s) => s.url).join(\",\")}</div>\n </div>\n );\n };\n\n const { getByTestId } = render(\n <TamboRegistryProvider>\n <TamboMcpProvider\n mcpServers={[{ url: \"https://one.example\" }, \"https://two.example\"]}\n >\n <Inner />\n </TamboMcpProvider>\n </TamboRegistryProvider>,\n );\n\n await waitFor(() => {\n expect(getByTestId(\"count\").textContent).toBe(\"2\");\n const urls = getByTestId(\"urls\").textContent || \"\";\n expect(urls).toContain(\"https://one.example\");\n expect(urls).toContain(\"https://two.example\");\n });\n });\n\n it(\"marks a successfully connected server with a client instance\", async () => {\n const fakeClient = { listTools: jest.fn().mockResolvedValue([]) } as any;\n createImpl.mockResolvedValue(fakeClient);\n\n let latest: McpServer[] = [];\n const Capture: React.FC = () => {\n const servers = useTamboMcpServers();\n useEffect(() => {\n latest = servers;\n }, [servers]);\n return null;\n };\n\n render(\n <TamboRegistryProvider>\n <TamboMcpProvider mcpServers={[{ url: \"https://ok.example\" }]}>\n <Capture />\n </TamboMcpProvider>\n </TamboRegistryProvider>,\n );\n\n await waitFor(() => {\n expect(latest.length).toBe(1);\n expect(\"client\" in latest[0]).toBe(true);\n expect((latest[0] as any).client).toBe(fakeClient);\n // no connectionError on connected server\n expect((latest[0] as any).connectionError).toBeUndefined();\n });\n });\n\n it(\"marks a failed server with a connectionError and no client\", async () => {\n const boom = new Error(\"boom\");\n createImpl.mockRejectedValue(boom);\n\n let latest: McpServer[] = [];\n const Capture: React.FC = () => {\n const servers = useTamboMcpServers();\n useEffect(() => {\n latest = servers;\n }, [servers]);\n return null;\n };\n\n render(\n <TamboRegistryProvider>\n <TamboMcpProvider mcpServers={[{ url: \"https://fail.example\" }]}>\n <Capture />\n </TamboMcpProvider>\n </TamboRegistryProvider>,\n );\n\n await waitFor(() => {\n expect(latest.length).toBe(1);\n expect(\"client\" in latest[0]).toBe(false);\n // @ts-expect-error narrowing at runtime\n expect(latest[0].connectionError).toBeInstanceOf(Error);\n // @ts-expect-error narrowing at runtime\n expect(latest[0].connectionError?.message).toBe(\"boom\");\n });\n });\n});\n"]}
|
package/esm/mcp/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { MCPTransport } from "./mcp-client";
|
|
2
|
-
export { TamboMcpProvider, type McpServerInfo } from "./tambo-mcp-provider";
|
|
2
|
+
export { TamboMcpProvider, useTamboMcpServers, type ConnectedMcpServer, type FailedMcpServer, type McpServer, type McpServerInfo, } from "./tambo-mcp-provider";
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/esm/mcp/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,aAAa,GACnB,MAAM,sBAAsB,CAAC"}
|
package/esm/mcp/index.js
CHANGED
package/esm/mcp/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACL,gBAAgB,EAChB,kBAAkB,GAKnB,MAAM,sBAAsB,CAAC","sourcesContent":["export { MCPTransport } from \"./mcp-client\";\nexport {\n TamboMcpProvider,\n useTamboMcpServers,\n type ConnectedMcpServer,\n type FailedMcpServer,\n type McpServer,\n type McpServerInfo,\n} from \"./tambo-mcp-provider\";\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { FC } from "react";
|
|
2
|
-
import { MCPTransport } from "./mcp-client";
|
|
1
|
+
import React, { FC } from "react";
|
|
2
|
+
import { MCPClient, MCPTransport } from "./mcp-client";
|
|
3
3
|
/**
|
|
4
4
|
* Extracts error message from MCP tool result content.
|
|
5
5
|
* Handles both array and string content formats.
|
|
@@ -14,6 +14,14 @@ export interface McpServerInfo {
|
|
|
14
14
|
transport?: MCPTransport;
|
|
15
15
|
customHeaders?: Record<string, string>;
|
|
16
16
|
}
|
|
17
|
+
export interface ConnectedMcpServer extends McpServerInfo {
|
|
18
|
+
client: MCPClient;
|
|
19
|
+
}
|
|
20
|
+
export interface FailedMcpServer extends McpServerInfo {
|
|
21
|
+
client?: never;
|
|
22
|
+
connectionError: Error;
|
|
23
|
+
}
|
|
24
|
+
export type McpServer = ConnectedMcpServer | FailedMcpServer;
|
|
17
25
|
/**
|
|
18
26
|
* This provider is used to register tools from MCP servers.
|
|
19
27
|
* @returns the wrapped children
|
|
@@ -22,4 +30,28 @@ export declare const TamboMcpProvider: FC<{
|
|
|
22
30
|
mcpServers: (McpServerInfo | string)[];
|
|
23
31
|
children: React.ReactNode;
|
|
24
32
|
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Hook to access the actual MCP servers, as they are connected (or fail to
|
|
35
|
+
* connect).
|
|
36
|
+
*
|
|
37
|
+
* You can call methods on the MCP client that is included in the MCP server
|
|
38
|
+
* object.
|
|
39
|
+
*
|
|
40
|
+
* If the server fails to connect, the `client` property will be `undefined` and
|
|
41
|
+
* the `connectionError` property will be set.
|
|
42
|
+
*
|
|
43
|
+
* For example, to forcibly disconnect and reconnect all MCP servers:
|
|
44
|
+
*
|
|
45
|
+
* ```tsx
|
|
46
|
+
* const mcpServers = useMcpServers();
|
|
47
|
+
* mcpServers.forEach((mcpServer) => {
|
|
48
|
+
* mcpServer.client?.reconnect();
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* Note that the MCP servers are not guaranteed to be in the same order as the
|
|
53
|
+
* input array, because they are added as they are connected.
|
|
54
|
+
* @returns The MCP servers
|
|
55
|
+
*/
|
|
56
|
+
export declare const useTamboMcpServers: () => McpServer[];
|
|
25
57
|
//# sourceMappingURL=tambo-mcp-provider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tambo-mcp-provider.d.ts","sourceRoot":"","sources":["../../src/mcp/tambo-mcp-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,
|
|
1
|
+
{"version":3,"file":"tambo-mcp-provider.d.ts","sourceRoot":"","sources":["../../src/mcp/tambo-mcp-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAEZ,EAAE,EAIH,MAAM,OAAO,CAAC;AAGf,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEvD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAsB5D;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,kBAAmB,SAAQ,aAAa;IACvD,MAAM,EAAE,SAAS,CAAC;CACnB;AAED,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,eAAe,EAAE,KAAK,CAAC;CACxB;AAED,MAAM,MAAM,SAAS,GAAG,kBAAkB,GAAG,eAAe,CAAC;AAG7D;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,EAAE,CAAC;IAChC,UAAU,EAAE,CAAC,aAAa,GAAG,MAAM,CAAC,EAAE,CAAC;IACvC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAsHA,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kBAAkB,mBAE9B,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
1
|
+
import React, { createContext, useContext, useEffect, useState, } from "react";
|
|
2
2
|
import { useTamboRegistry } from "../providers/tambo-registry-provider";
|
|
3
3
|
import { MCPClient, MCPTransport } from "./mcp-client";
|
|
4
4
|
/**
|
|
@@ -24,28 +24,54 @@ export function extractErrorMessage(content) {
|
|
|
24
24
|
}
|
|
25
25
|
return `${content}`;
|
|
26
26
|
}
|
|
27
|
+
const McpProviderContext = createContext([]);
|
|
27
28
|
/**
|
|
28
29
|
* This provider is used to register tools from MCP servers.
|
|
29
30
|
* @returns the wrapped children
|
|
30
31
|
*/
|
|
31
32
|
export const TamboMcpProvider = ({ mcpServers, children }) => {
|
|
32
33
|
const { registerTool } = useTamboRegistry();
|
|
34
|
+
const [connectedMcpServers, setConnectedMcpServers] = useState([]);
|
|
33
35
|
useEffect(() => {
|
|
34
36
|
if (!mcpServers) {
|
|
35
37
|
return;
|
|
36
38
|
}
|
|
37
|
-
async function registerMcpServers(
|
|
39
|
+
async function registerMcpServers(mcpServerInfos) {
|
|
38
40
|
// Maps tool names to the MCP client that registered them
|
|
39
41
|
const mcpServerMap = new Map();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
// initialize the MCP clients, converting McpServerInfo -> McpServer
|
|
43
|
+
const mcpServers = await Promise.allSettled(mcpServerInfos.map(async (mcpServerInfo) => {
|
|
44
|
+
try {
|
|
45
|
+
const client = await MCPClient.create(mcpServerInfo.url, mcpServerInfo.transport, mcpServerInfo.customHeaders);
|
|
46
|
+
const connectedMcpServer = {
|
|
47
|
+
...mcpServerInfo,
|
|
48
|
+
client: client,
|
|
49
|
+
};
|
|
50
|
+
// note because the promises may resolve in any order, the resulting
|
|
51
|
+
// array may not be in the same order as the input array
|
|
52
|
+
setConnectedMcpServers((prev) => [...prev, connectedMcpServer]);
|
|
53
|
+
return connectedMcpServer;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const failedMcpServer = {
|
|
57
|
+
...mcpServerInfo,
|
|
58
|
+
connectionError: error,
|
|
59
|
+
};
|
|
60
|
+
// note because the promises may resolve in any order, the resulting
|
|
61
|
+
// array may not be in the same order as the input array
|
|
62
|
+
setConnectedMcpServers((prev) => [...prev, failedMcpServer]);
|
|
63
|
+
return failedMcpServer;
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
// note do not rely on the state
|
|
67
|
+
const connectedMcpServers = mcpServers
|
|
68
|
+
.filter((result) => result.status === "fulfilled")
|
|
69
|
+
.map((result) => result.value);
|
|
70
|
+
// Now create a map of tool name to MCP client
|
|
71
|
+
const serverToolLists = connectedMcpServers.map(async (mcpServer) => {
|
|
72
|
+
const tools = (await mcpServer.client?.listTools()) ?? [];
|
|
47
73
|
tools.forEach((tool) => {
|
|
48
|
-
mcpServerMap.set(tool.name,
|
|
74
|
+
mcpServerMap.set(tool.name, mcpServer);
|
|
49
75
|
});
|
|
50
76
|
return tools;
|
|
51
77
|
});
|
|
@@ -70,7 +96,11 @@ export const TamboMcpProvider = ({ mcpServers, children }) => {
|
|
|
70
96
|
// should never happen
|
|
71
97
|
throw new Error(`MCP server for tool ${tool.name} not found`);
|
|
72
98
|
}
|
|
73
|
-
|
|
99
|
+
if (!mcpServer.client) {
|
|
100
|
+
// this can't actually happen because the tool can't be registered if the server is not connected
|
|
101
|
+
throw new Error(`MCP server for tool ${tool.name} is not connected`);
|
|
102
|
+
}
|
|
103
|
+
const result = await mcpServer.client.callTool(tool.name, args);
|
|
74
104
|
if (result.isError) {
|
|
75
105
|
const errorMessage = extractErrorMessage(result.content);
|
|
76
106
|
throw new Error(errorMessage);
|
|
@@ -81,8 +111,38 @@ export const TamboMcpProvider = ({ mcpServers, children }) => {
|
|
|
81
111
|
});
|
|
82
112
|
});
|
|
83
113
|
}
|
|
84
|
-
|
|
114
|
+
// normalize the server infos
|
|
115
|
+
const mcpServerInfos = mcpServers.map((mcpServer) => typeof mcpServer === "string"
|
|
116
|
+
? { url: mcpServer, transport: MCPTransport.SSE }
|
|
117
|
+
: mcpServer);
|
|
118
|
+
registerMcpServers(mcpServerInfos);
|
|
85
119
|
}, [mcpServers, registerTool]);
|
|
86
|
-
return children;
|
|
120
|
+
return (React.createElement(McpProviderContext.Provider, { value: connectedMcpServers }, children));
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Hook to access the actual MCP servers, as they are connected (or fail to
|
|
124
|
+
* connect).
|
|
125
|
+
*
|
|
126
|
+
* You can call methods on the MCP client that is included in the MCP server
|
|
127
|
+
* object.
|
|
128
|
+
*
|
|
129
|
+
* If the server fails to connect, the `client` property will be `undefined` and
|
|
130
|
+
* the `connectionError` property will be set.
|
|
131
|
+
*
|
|
132
|
+
* For example, to forcibly disconnect and reconnect all MCP servers:
|
|
133
|
+
*
|
|
134
|
+
* ```tsx
|
|
135
|
+
* const mcpServers = useMcpServers();
|
|
136
|
+
* mcpServers.forEach((mcpServer) => {
|
|
137
|
+
* mcpServer.client?.reconnect();
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* Note that the MCP servers are not guaranteed to be in the same order as the
|
|
142
|
+
* input array, because they are added as they are connected.
|
|
143
|
+
* @returns The MCP servers
|
|
144
|
+
*/
|
|
145
|
+
export const useTamboMcpServers = () => {
|
|
146
|
+
return useContext(McpProviderContext);
|
|
87
147
|
};
|
|
88
148
|
//# sourceMappingURL=tambo-mcp-provider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tambo-mcp-provider.js","sourceRoot":"","sources":["../../src/mcp/tambo-mcp-provider.tsx"],"names":[],"mappings":"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","sourcesContent":["import { FC, useEffect } from \"react\";\nimport { TamboTool } from \"../model/component-metadata\";\nimport { useTamboRegistry } from \"../providers/tambo-registry-provider\";\nimport { MCPClient, MCPTransport } from \"./mcp-client\";\n\n/**\n * Extracts error message from MCP tool result content.\n * Handles both array and string content formats.\n * Always returns a string, even for invalid/null inputs.\n * @returns The extracted error message as a string\n */\nexport function extractErrorMessage(content: unknown): string {\n if (content === undefined || content === null) {\n return \"Unknown error occurred\";\n }\n\n if (Array.isArray(content)) {\n const textItems = content\n .filter(\n (item) => item && item.type === \"text\" && typeof item.text === \"string\",\n )\n .map((item) => item.text);\n\n return textItems.length > 0\n ? textItems.join(\" \")\n : \"Error occurred but no details provided\";\n }\n\n if (typeof content === \"object\") {\n return JSON.stringify(content);\n }\n\n return `${content}`;\n}\n\nexport interface McpServerInfo {\n name?: string;\n url: string;\n description?: string;\n transport?: MCPTransport;\n customHeaders?: Record<string, string>;\n}\n/**\n * This provider is used to register tools from MCP servers.\n * @returns the wrapped children\n */\nexport const TamboMcpProvider: FC<{\n mcpServers: (McpServerInfo | string)[];\n children: React.ReactNode;\n}> = ({ mcpServers, children }) => {\n const { registerTool } = useTamboRegistry();\n\n useEffect(() => {\n if (!mcpServers) {\n return;\n }\n async function registerMcpServers(mcpServers: (McpServerInfo | string)[]) {\n // Maps tool names to the MCP client that registered them\n const mcpServerMap = new Map<string, MCPClient>();\n const serverToolLists = mcpServers.map(async (mcpServer) => {\n const server =\n typeof mcpServer === \"string\"\n ? { url: mcpServer, transport: MCPTransport.SSE }\n : mcpServer;\n const { url, transport = MCPTransport.SSE, customHeaders } = server;\n const mcpClient = await MCPClient.create(url, transport, customHeaders);\n const tools = await mcpClient.listTools();\n tools.forEach((tool) => {\n mcpServerMap.set(tool.name, mcpClient);\n });\n return tools;\n });\n const toolResults = await Promise.allSettled(serverToolLists);\n\n // Just log the failed tools, we can't do anything about them\n const failedTools = toolResults.filter(\n (result) => result.status === \"rejected\",\n );\n if (failedTools.length > 0) {\n console.error(\n \"Failed to register tools from MCP servers:\",\n failedTools.map((result) => result.reason),\n );\n }\n\n // Register the successful tools\n const allTools = toolResults\n .filter((result) => result.status === \"fulfilled\")\n .map((result) => result.value)\n .flat();\n allTools.forEach((tool) => {\n registerTool({\n description: tool.description ?? \"\",\n name: tool.name,\n tool: async (args: Record<string, unknown>) => {\n const mcpServer = mcpServerMap.get(tool.name);\n if (!mcpServer) {\n // should never happen\n throw new Error(`MCP server for tool ${tool.name} not found`);\n }\n const result = await mcpServer.callTool(tool.name, args);\n if (result.isError) {\n const errorMessage = extractErrorMessage(result.content);\n throw new Error(errorMessage);\n }\n return result.content;\n },\n toolSchema: tool.inputSchema as TamboTool[\"toolSchema\"],\n });\n });\n }\n registerMcpServers(mcpServers);\n }, [mcpServers, registerTool]);\n\n return children;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"tambo-mcp-provider.js","sourceRoot":"","sources":["../../src/mcp/tambo-mcp-provider.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, {\n createContext,\n FC,\n useContext,\n useEffect,\n useState,\n} from \"react\";\nimport { TamboTool } from \"../model/component-metadata\";\nimport { useTamboRegistry } from \"../providers/tambo-registry-provider\";\nimport { MCPClient, MCPTransport } from \"./mcp-client\";\n\n/**\n * Extracts error message from MCP tool result content.\n * Handles both array and string content formats.\n * Always returns a string, even for invalid/null inputs.\n * @returns The extracted error message as a string\n */\nexport function extractErrorMessage(content: unknown): string {\n if (content === undefined || content === null) {\n return \"Unknown error occurred\";\n }\n\n if (Array.isArray(content)) {\n const textItems = content\n .filter(\n (item) => item && item.type === \"text\" && typeof item.text === \"string\",\n )\n .map((item) => item.text);\n\n return textItems.length > 0\n ? textItems.join(\" \")\n : \"Error occurred but no details provided\";\n }\n\n if (typeof content === \"object\") {\n return JSON.stringify(content);\n }\n\n return `${content}`;\n}\n\nexport interface McpServerInfo {\n name?: string;\n url: string;\n description?: string;\n transport?: MCPTransport;\n customHeaders?: Record<string, string>;\n}\n\nexport interface ConnectedMcpServer extends McpServerInfo {\n client: MCPClient;\n}\n\nexport interface FailedMcpServer extends McpServerInfo {\n client?: never;\n connectionError: Error;\n}\n\nexport type McpServer = ConnectedMcpServer | FailedMcpServer;\n\nconst McpProviderContext = createContext<McpServer[]>([]);\n/**\n * This provider is used to register tools from MCP servers.\n * @returns the wrapped children\n */\nexport const TamboMcpProvider: FC<{\n mcpServers: (McpServerInfo | string)[];\n children: React.ReactNode;\n}> = ({ mcpServers, children }) => {\n const { registerTool } = useTamboRegistry();\n const [connectedMcpServers, setConnectedMcpServers] = useState<McpServer[]>(\n [],\n );\n\n useEffect(() => {\n if (!mcpServers) {\n return;\n }\n async function registerMcpServers(mcpServerInfos: McpServerInfo[]) {\n // Maps tool names to the MCP client that registered them\n const mcpServerMap = new Map<string, McpServer>();\n\n // initialize the MCP clients, converting McpServerInfo -> McpServer\n const mcpServers = await Promise.allSettled(\n mcpServerInfos.map(async (mcpServerInfo): Promise<McpServer> => {\n try {\n const client = await MCPClient.create(\n mcpServerInfo.url,\n mcpServerInfo.transport,\n mcpServerInfo.customHeaders,\n );\n const connectedMcpServer = {\n ...mcpServerInfo,\n client: client,\n };\n // note because the promises may resolve in any order, the resulting\n // array may not be in the same order as the input array\n setConnectedMcpServers((prev) => [...prev, connectedMcpServer]);\n return connectedMcpServer;\n } catch (error) {\n const failedMcpServer = {\n ...mcpServerInfo,\n connectionError: error as Error,\n };\n // note because the promises may resolve in any order, the resulting\n // array may not be in the same order as the input array\n setConnectedMcpServers((prev) => [...prev, failedMcpServer]);\n return failedMcpServer;\n }\n }),\n );\n\n // note do not rely on the state\n const connectedMcpServers = mcpServers\n .filter((result) => result.status === \"fulfilled\")\n .map((result) => result.value);\n\n // Now create a map of tool name to MCP client\n const serverToolLists = connectedMcpServers.map(async (mcpServer) => {\n const tools = (await mcpServer.client?.listTools()) ?? [];\n tools.forEach((tool) => {\n mcpServerMap.set(tool.name, mcpServer);\n });\n return tools;\n });\n const toolResults = await Promise.allSettled(serverToolLists);\n\n // Just log the failed tools, we can't do anything about them\n const failedTools = toolResults.filter(\n (result) => result.status === \"rejected\",\n );\n if (failedTools.length > 0) {\n console.error(\n \"Failed to register tools from MCP servers:\",\n failedTools.map((result) => result.reason),\n );\n }\n\n // Register the successful tools\n const allTools = toolResults\n .filter((result) => result.status === \"fulfilled\")\n .map((result) => result.value)\n .flat();\n allTools.forEach((tool) => {\n registerTool({\n description: tool.description ?? \"\",\n name: tool.name,\n tool: async (args: Record<string, unknown>) => {\n const mcpServer = mcpServerMap.get(tool.name);\n if (!mcpServer) {\n // should never happen\n throw new Error(`MCP server for tool ${tool.name} not found`);\n }\n if (!mcpServer.client) {\n // this can't actually happen because the tool can't be registered if the server is not connected\n throw new Error(\n `MCP server for tool ${tool.name} is not connected`,\n );\n }\n const result = await mcpServer.client.callTool(tool.name, args);\n if (result.isError) {\n const errorMessage = extractErrorMessage(result.content);\n throw new Error(errorMessage);\n }\n return result.content;\n },\n toolSchema: tool.inputSchema as TamboTool[\"toolSchema\"],\n });\n });\n }\n\n // normalize the server infos\n const mcpServerInfos = mcpServers.map((mcpServer) =>\n typeof mcpServer === \"string\"\n ? { url: mcpServer, transport: MCPTransport.SSE }\n : mcpServer,\n );\n\n registerMcpServers(mcpServerInfos);\n }, [mcpServers, registerTool]);\n\n return (\n <McpProviderContext.Provider value={connectedMcpServers}>\n {children}\n </McpProviderContext.Provider>\n );\n};\n\n/**\n * Hook to access the actual MCP servers, as they are connected (or fail to\n * connect).\n *\n * You can call methods on the MCP client that is included in the MCP server\n * object.\n *\n * If the server fails to connect, the `client` property will be `undefined` and\n * the `connectionError` property will be set.\n *\n * For example, to forcibly disconnect and reconnect all MCP servers:\n *\n * ```tsx\n * const mcpServers = useMcpServers();\n * mcpServers.forEach((mcpServer) => {\n * mcpServer.client?.reconnect();\n * });\n * ```\n *\n * Note that the MCP servers are not guaranteed to be in the same order as the\n * input array, because they are added as they are connected.\n * @returns The MCP servers\n */\nexport const useTamboMcpServers = () => {\n return useContext(McpProviderContext);\n};\n"]}
|