@tambo-ai/react 0.58.1 → 0.60.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.
Files changed (119) hide show
  1. package/dist/hooks/react-query-hooks.d.ts +14 -1
  2. package/dist/hooks/react-query-hooks.d.ts.map +1 -1
  3. package/dist/hooks/react-query-hooks.js +13 -0
  4. package/dist/hooks/react-query-hooks.js.map +1 -1
  5. package/dist/hooks/use-tambo-stream-status.js +1 -1
  6. package/dist/hooks/use-tambo-stream-status.js.map +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/mcp/__tests__/elicitation.test.d.ts +2 -0
  10. package/dist/mcp/__tests__/elicitation.test.d.ts.map +1 -0
  11. package/dist/mcp/__tests__/elicitation.test.js +261 -0
  12. package/dist/mcp/__tests__/elicitation.test.js.map +1 -0
  13. package/dist/mcp/__tests__/mcp-client.test.js +0 -266
  14. package/dist/mcp/__tests__/mcp-client.test.js.map +1 -1
  15. package/dist/mcp/__tests__/mcp-hooks.test.d.ts +2 -0
  16. package/dist/mcp/__tests__/mcp-hooks.test.d.ts.map +1 -0
  17. package/dist/mcp/__tests__/mcp-hooks.test.js +504 -0
  18. package/dist/mcp/__tests__/mcp-hooks.test.js.map +1 -0
  19. package/dist/mcp/__tests__/tambo-mcp-provider.test.js +361 -16
  20. package/dist/mcp/__tests__/tambo-mcp-provider.test.js.map +1 -1
  21. package/dist/mcp/__tests__/use-mcp-servers.test.js +34 -9
  22. package/dist/mcp/__tests__/use-mcp-servers.test.js.map +1 -1
  23. package/dist/mcp/elicitation.d.ts +80 -0
  24. package/dist/mcp/elicitation.d.ts.map +1 -0
  25. package/dist/mcp/elicitation.js +55 -0
  26. package/dist/mcp/elicitation.js.map +1 -0
  27. package/dist/mcp/index.d.ts +4 -1
  28. package/dist/mcp/index.d.ts.map +1 -1
  29. package/dist/mcp/index.js +5 -1
  30. package/dist/mcp/index.js.map +1 -1
  31. package/dist/mcp/mcp-client.d.ts +51 -86
  32. package/dist/mcp/mcp-client.d.ts.map +1 -1
  33. package/dist/mcp/mcp-client.js +22 -159
  34. package/dist/mcp/mcp-client.js.map +1 -1
  35. package/dist/mcp/mcp-hooks.d.ts +107 -0
  36. package/dist/mcp/mcp-hooks.d.ts.map +1 -0
  37. package/dist/mcp/mcp-hooks.js +103 -0
  38. package/dist/mcp/mcp-hooks.js.map +1 -0
  39. package/dist/mcp/tambo-mcp-provider.d.ts +86 -4
  40. package/dist/mcp/tambo-mcp-provider.d.ts.map +1 -1
  41. package/dist/mcp/tambo-mcp-provider.js +275 -106
  42. package/dist/mcp/tambo-mcp-provider.js.map +1 -1
  43. package/dist/providers/__tests__/tambo-thread-provider-initial-messages.test.js +3 -1
  44. package/dist/providers/__tests__/tambo-thread-provider-initial-messages.test.js.map +1 -1
  45. package/dist/providers/__tests__/tambo-thread-provider.test.js +25 -12
  46. package/dist/providers/__tests__/tambo-thread-provider.test.js.map +1 -1
  47. package/dist/providers/tambo-interactable-provider.d.ts.map +1 -1
  48. package/dist/providers/tambo-interactable-provider.js +11 -4
  49. package/dist/providers/tambo-interactable-provider.js.map +1 -1
  50. package/dist/providers/tambo-mcp-token-provider.d.ts +34 -0
  51. package/dist/providers/tambo-mcp-token-provider.d.ts.map +1 -0
  52. package/dist/providers/tambo-mcp-token-provider.js +69 -0
  53. package/dist/providers/tambo-mcp-token-provider.js.map +1 -0
  54. package/dist/providers/tambo-provider.d.ts.map +1 -1
  55. package/dist/providers/tambo-provider.js +7 -9
  56. package/dist/providers/tambo-provider.js.map +1 -1
  57. package/dist/providers/tambo-thread-provider.d.ts.map +1 -1
  58. package/dist/providers/tambo-thread-provider.js +14 -0
  59. package/dist/providers/tambo-thread-provider.js.map +1 -1
  60. package/esm/hooks/react-query-hooks.d.ts +14 -1
  61. package/esm/hooks/react-query-hooks.d.ts.map +1 -1
  62. package/esm/hooks/react-query-hooks.js +13 -1
  63. package/esm/hooks/react-query-hooks.js.map +1 -1
  64. package/esm/hooks/use-tambo-stream-status.js +1 -1
  65. package/esm/hooks/use-tambo-stream-status.js.map +1 -1
  66. package/esm/index.js +2 -0
  67. package/esm/index.js.map +1 -1
  68. package/esm/mcp/__tests__/elicitation.test.d.ts +2 -0
  69. package/esm/mcp/__tests__/elicitation.test.d.ts.map +1 -0
  70. package/esm/mcp/__tests__/elicitation.test.js +259 -0
  71. package/esm/mcp/__tests__/elicitation.test.js.map +1 -0
  72. package/esm/mcp/__tests__/mcp-client.test.js +0 -266
  73. package/esm/mcp/__tests__/mcp-client.test.js.map +1 -1
  74. package/esm/mcp/__tests__/mcp-hooks.test.d.ts +2 -0
  75. package/esm/mcp/__tests__/mcp-hooks.test.d.ts.map +1 -0
  76. package/esm/mcp/__tests__/mcp-hooks.test.js +469 -0
  77. package/esm/mcp/__tests__/mcp-hooks.test.js.map +1 -0
  78. package/esm/mcp/__tests__/tambo-mcp-provider.test.js +361 -16
  79. package/esm/mcp/__tests__/tambo-mcp-provider.test.js.map +1 -1
  80. package/esm/mcp/__tests__/use-mcp-servers.test.js +34 -9
  81. package/esm/mcp/__tests__/use-mcp-servers.test.js.map +1 -1
  82. package/esm/mcp/elicitation.d.ts +80 -0
  83. package/esm/mcp/elicitation.d.ts.map +1 -0
  84. package/esm/mcp/elicitation.js +52 -0
  85. package/esm/mcp/elicitation.js.map +1 -0
  86. package/esm/mcp/index.d.ts +4 -1
  87. package/esm/mcp/index.d.ts.map +1 -1
  88. package/esm/mcp/index.js +2 -1
  89. package/esm/mcp/index.js.map +1 -1
  90. package/esm/mcp/mcp-client.d.ts +51 -86
  91. package/esm/mcp/mcp-client.d.ts.map +1 -1
  92. package/esm/mcp/mcp-client.js +22 -159
  93. package/esm/mcp/mcp-client.js.map +1 -1
  94. package/esm/mcp/mcp-hooks.d.ts +107 -0
  95. package/esm/mcp/mcp-hooks.d.ts.map +1 -0
  96. package/esm/mcp/mcp-hooks.js +99 -0
  97. package/esm/mcp/mcp-hooks.js.map +1 -0
  98. package/esm/mcp/tambo-mcp-provider.d.ts +86 -4
  99. package/esm/mcp/tambo-mcp-provider.d.ts.map +1 -1
  100. package/esm/mcp/tambo-mcp-provider.js +275 -107
  101. package/esm/mcp/tambo-mcp-provider.js.map +1 -1
  102. package/esm/providers/__tests__/tambo-thread-provider-initial-messages.test.js +3 -1
  103. package/esm/providers/__tests__/tambo-thread-provider-initial-messages.test.js.map +1 -1
  104. package/esm/providers/__tests__/tambo-thread-provider.test.js +25 -12
  105. package/esm/providers/__tests__/tambo-thread-provider.test.js.map +1 -1
  106. package/esm/providers/tambo-interactable-provider.d.ts.map +1 -1
  107. package/esm/providers/tambo-interactable-provider.js +11 -4
  108. package/esm/providers/tambo-interactable-provider.js.map +1 -1
  109. package/esm/providers/tambo-mcp-token-provider.d.ts +34 -0
  110. package/esm/providers/tambo-mcp-token-provider.d.ts.map +1 -0
  111. package/esm/providers/tambo-mcp-token-provider.js +31 -0
  112. package/esm/providers/tambo-mcp-token-provider.js.map +1 -0
  113. package/esm/providers/tambo-provider.d.ts.map +1 -1
  114. package/esm/providers/tambo-provider.js +7 -9
  115. package/esm/providers/tambo-provider.js.map +1 -1
  116. package/esm/providers/tambo-thread-provider.d.ts.map +1 -1
  117. package/esm/providers/tambo-thread-provider.js +14 -0
  118. package/esm/providers/tambo-thread-provider.js.map +1 -1
  119. package/package.json +8 -8
@@ -1,5 +1,5 @@
1
1
  import React, { FC } from "react";
2
- import { MCPClient, MCPTransport } from "./mcp-client";
2
+ import { MCPClient, MCPElicitationHandler, MCPHandlers, MCPSamplingHandler, 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.
@@ -7,27 +7,79 @@ import { MCPClient, MCPTransport } from "./mcp-client";
7
7
  * @returns The extracted error message as a string
8
8
  */
9
9
  export declare function extractErrorMessage(content: unknown): string;
10
+ /**
11
+ * User-provided configuration for an MCP server.
12
+ */
10
13
  export interface McpServerInfo {
14
+ /** Optional name for the MCP server */
11
15
  name?: string;
16
+ /** The URL of the MCP server to connect to */
12
17
  url: string;
18
+ /** Optional description of the MCP server */
13
19
  description?: string;
20
+ /** The transport type to use (SSE or HTTP). Defaults to SSE for string URLs */
14
21
  transport?: MCPTransport;
22
+ /** Optional custom headers to include in requests */
15
23
  customHeaders?: Record<string, string>;
24
+ /**
25
+ * Optional handlers for elicitation and sampling requests from the server.
26
+ * Note: These callbacks should be stable (e.g., wrapped in useCallback or defined outside the component)
27
+ * to avoid constant re-registration of the MCP server on every render.
28
+ */
29
+ handlers?: Partial<MCPHandlers>;
30
+ }
31
+ /**
32
+ * Normalized server information with a stable derived key.
33
+ */
34
+ interface McpServerConfig extends McpServerInfo {
35
+ /**
36
+ * Stable identity for this server derived from its URL/transport/headers.
37
+ * Present for all server states (connected or failed).
38
+ */
39
+ key: string;
16
40
  }
17
- export interface ConnectedMcpServer extends McpServerInfo {
41
+ /**
42
+ * Connected MCP server with an active client.
43
+ */
44
+ export interface ConnectedMcpServer extends McpServerConfig {
18
45
  client: MCPClient;
19
46
  }
20
- export interface FailedMcpServer extends McpServerInfo {
47
+ /**
48
+ * Failed MCP server with a connection error.
49
+ */
50
+ export interface FailedMcpServer extends McpServerConfig {
21
51
  client?: never;
22
52
  connectionError: Error;
23
53
  }
54
+ /**
55
+ * An active or failed MCP server, with access to the MCP client.
56
+ */
24
57
  export type McpServer = ConnectedMcpServer | FailedMcpServer;
58
+ /**
59
+ * Provider-level MCP handlers that receive the McpServerInfo as context in addition to the request.
60
+ * These handlers are applied to all MCP servers unless overridden by per-server handlers.
61
+ *
62
+ * Handlers receive three parameters:
63
+ * 1. request - The MCP request
64
+ * 2. extra - RequestHandlerExtra containing AbortSignal and other metadata
65
+ * 3. serverInfo - Configuration of the MCP server that triggered this request
66
+ */
67
+ export interface ProviderMCPHandlers {
68
+ elicitation?: (request: Parameters<MCPElicitationHandler>[0], extra: Parameters<MCPElicitationHandler>[1], serverInfo: McpServerConfig) => ReturnType<MCPElicitationHandler>;
69
+ sampling?: (request: Parameters<MCPSamplingHandler>[0], extra: Parameters<MCPSamplingHandler>[1], serverInfo: McpServerConfig) => ReturnType<MCPSamplingHandler>;
70
+ }
25
71
  /**
26
72
  * This provider is used to register tools from MCP servers.
27
- * @returns the wrapped children
73
+ * It automatically includes an internal Tambo MCP server when an MCP access token is available.
74
+ * @param props - The provider props
75
+ * @param props.mcpServers - Array of MCP server configurations
76
+ * @param props.handlers - Optional handlers applied to all MCP servers unless overridden per-server
77
+ * @param props.children - The children to wrap
78
+ * @returns The TamboMcpProvider component
28
79
  */
29
80
  export declare const TamboMcpProvider: FC<{
30
81
  mcpServers: (McpServerInfo | string)[];
82
+ handlers?: ProviderMCPHandlers;
31
83
  children: React.ReactNode;
32
84
  }>;
33
85
  /**
@@ -54,4 +106,34 @@ export declare const TamboMcpProvider: FC<{
54
106
  * @returns The MCP servers
55
107
  */
56
108
  export declare const useTamboMcpServers: () => McpServer[];
109
+ /**
110
+ * Hook to access elicitation context from TamboMcpProvider.
111
+ * This provides access to the current elicitation request and methods to respond to it.
112
+ *
113
+ * The elicitation state is automatically managed by TamboMcpProvider when MCP servers
114
+ * request user input through the elicitation protocol.
115
+ * @returns The elicitation context with current request and response handlers
116
+ * @example
117
+ * ```tsx
118
+ * function ElicitationUI() {
119
+ * const { elicitation, resolveElicitation } = useTamboElicitationContext();
120
+ *
121
+ * if (!elicitation) return null;
122
+ *
123
+ * return (
124
+ * <div>
125
+ * <p>{elicitation.message}</p>
126
+ * <button onClick={() => resolveElicitation?.({ action: "accept", content: {} })}>
127
+ * Accept
128
+ * </button>
129
+ * </div>
130
+ * );
131
+ * }
132
+ * ```
133
+ */
134
+ export declare const useTamboElicitationContext: () => {
135
+ elicitation: import("./elicitation").TamboElicitationRequest | null;
136
+ resolveElicitation: ((response: import("./elicitation").TamboElicitationResponse) => void) | null;
137
+ };
138
+ export {};
57
139
  //# 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":"AACA,OAAO,KAAK,EAAE,EAEZ,EAAE,EAIH,MAAM,OAAO,CAAC;AAIf,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,CAgJA,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kBAAkB,mBAE9B,CAAC"}
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,EAMH,MAAM,OAAO,CAAC;AAMf,OAAO,EACL,SAAS,EACT,qBAAqB,EACrB,WAAW,EACX,kBAAkB,EAClB,YAAY,EACb,MAAM,cAAc,CAAC;AAEtB;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAoB5D;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,qDAAqD;IACrD,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,UAAU,eAAgB,SAAQ,aAAa;IAC7C;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,MAAM,EAAE,SAAS,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,eAAe,EAAE,KAAK,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,kBAAkB,GAAG,eAAe,CAAC;AAE7D;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IAClC,WAAW,CAAC,EAAE,CACZ,OAAO,EAAE,UAAU,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,EAC7C,KAAK,EAAE,UAAU,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,EAC3C,UAAU,EAAE,eAAe,KACxB,UAAU,CAAC,qBAAqB,CAAC,CAAC;IACvC,QAAQ,CAAC,EAAE,CACT,OAAO,EAAE,UAAU,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,EAC1C,KAAK,EAAE,UAAU,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,EACxC,UAAU,EAAE,eAAe,KACxB,UAAU,CAAC,kBAAkB,CAAC,CAAC;CACrC;AAkBD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,EAAE,EAAE,CAAC;IAChC,UAAU,EAAE,CAAC,aAAa,GAAG,MAAM,CAAC,EAAE,CAAC;IACvC,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CA2RA,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kBAAkB,mBAE9B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,0BAA0B;;;CAMtC,CAAC"}
@@ -33,12 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.useTamboMcpServers = exports.TamboMcpProvider = void 0;
36
+ exports.useTamboElicitationContext = exports.useTamboMcpServers = exports.TamboMcpProvider = void 0;
37
37
  exports.extractErrorMessage = extractErrorMessage;
38
- const fast_equals_1 = require("fast-equals");
39
38
  const react_1 = __importStar(require("react"));
39
+ const tambo_mcp_token_provider_1 = require("../providers/tambo-mcp-token-provider");
40
40
  const tambo_registry_provider_1 = require("../providers/tambo-registry-provider");
41
41
  const content_parts_1 = require("../util/content-parts");
42
+ const elicitation_1 = require("./elicitation");
42
43
  const mcp_client_1 = require("./mcp-client");
43
44
  /**
44
45
  * Extracts error message from MCP tool result content.
@@ -52,7 +53,7 @@ function extractErrorMessage(content) {
52
53
  }
53
54
  if (Array.isArray(content)) {
54
55
  const textItems = content
55
- .filter((item) => item && item.type === "text" && typeof item.text === "string")
56
+ .filter((item) => item?.type === "text" && typeof item.text === "string")
56
57
  .map((item) => item.text);
57
58
  return textItems.length > 0
58
59
  ? textItems.join(" ")
@@ -63,120 +64,236 @@ function extractErrorMessage(content) {
63
64
  }
64
65
  return `${content}`;
65
66
  }
66
- const McpProviderContext = (0, react_1.createContext)([]);
67
+ const McpProviderContext = (0, react_1.createContext)({
68
+ servers: [],
69
+ elicitation: null,
70
+ resolveElicitation: null,
71
+ });
72
+ // Constant for the internal Tambo MCP server name
73
+ const TAMBO_INTERNAL_MCP_SERVER_NAME = "__tambo_internal_mcp_server__";
67
74
  /**
68
75
  * This provider is used to register tools from MCP servers.
69
- * @returns the wrapped children
76
+ * It automatically includes an internal Tambo MCP server when an MCP access token is available.
77
+ * @param props - The provider props
78
+ * @param props.mcpServers - Array of MCP server configurations
79
+ * @param props.handlers - Optional handlers applied to all MCP servers unless overridden per-server
80
+ * @param props.children - The children to wrap
81
+ * @returns The TamboMcpProvider component
70
82
  */
71
- const TamboMcpProvider = ({ mcpServers, children }) => {
83
+ const TamboMcpProvider = ({ mcpServers, handlers, children }) => {
72
84
  const { registerTool } = (0, tambo_registry_provider_1.useTamboRegistry)();
85
+ const { mcpAccessToken, tamboBaseUrl } = (0, tambo_mcp_token_provider_1.useTamboMcpToken)();
86
+ const providerSamplingHandler = handlers?.sampling;
87
+ // Elicitation state and default handler
88
+ const { elicitation, resolveElicitation, defaultElicitationHandler } = (0, elicitation_1.useElicitation)();
89
+ // Use provided handler or fall back to default
90
+ const providerElicitationHandler = handlers?.elicitation ?? defaultElicitationHandler;
91
+ // Stable reference to track active clients by server key
92
+ const clientMapRef = (0, react_1.useRef)(new Map());
93
+ // Track tool ownership to prevent duplicate registrations across servers
94
+ // toolOwnerRef: tool name -> server key; keyToToolsRef: server key -> set of tool names
95
+ const toolOwnerRef = (0, react_1.useRef)(new Map());
96
+ const keyToToolsRef = (0, react_1.useRef)(new Map());
97
+ // State for exposing connected servers to consumers
73
98
  const [connectedMcpServers, setConnectedMcpServers] = (0, react_1.useState)([]);
74
- (0, react_1.useEffect)(() => {
75
- if (!mcpServers) {
76
- return;
99
+ // Stable map of current server configurations keyed by server key
100
+ const currentServersMap = (0, react_1.useMemo)(() => {
101
+ const servers = [...mcpServers];
102
+ // Add internal Tambo MCP server if we have an access token and a base URL
103
+ if (mcpAccessToken && tamboBaseUrl) {
104
+ const base = new URL(tamboBaseUrl);
105
+ base.pathname = `${base.pathname.replace(/\/+$/, "")}/mcp`;
106
+ const tamboMcpUrl = base.toString();
107
+ servers.push({
108
+ name: TAMBO_INTERNAL_MCP_SERVER_NAME,
109
+ url: tamboMcpUrl,
110
+ transport: mcp_client_1.MCPTransport.HTTP,
111
+ customHeaders: {
112
+ Authorization: `Bearer ${mcpAccessToken}`,
113
+ },
114
+ });
77
115
  }
78
- async function registerMcpServers(mcpServerInfos) {
79
- // Maps tool names to the MCP client that registered them
80
- const mcpServerMap = new Map();
81
- setConnectedMcpServers((prev) =>
82
- // remove any servers that are not in the new list
83
- prev.filter((s) => mcpServerInfos.some((mcpServerInfo) => equalsMcpServer(s, mcpServerInfo))));
84
- // initialize the MCP clients, converting McpServerInfo -> McpServer
85
- const mcpServers = await Promise.allSettled(mcpServerInfos.map(async (mcpServerInfo) => {
116
+ // Create a map of server key -> server info for efficient lookups
117
+ const serverMap = new Map();
118
+ servers.forEach((server) => {
119
+ const serverInfo = normalizeServerInfo(server);
120
+ // Store without cloning to avoid unnecessary allocation
121
+ serverMap.set(serverInfo.key, serverInfo);
122
+ });
123
+ return serverMap;
124
+ }, [mcpServers, mcpAccessToken, tamboBaseUrl]);
125
+ // Main effect: manage client lifecycle (create/remove)
126
+ (0, react_1.useEffect)(() => {
127
+ const clientMap = clientMapRef.current;
128
+ const currentKeys = new Set(currentServersMap.keys());
129
+ const existingKeys = new Set(clientMap.keys());
130
+ // 1. Remove clients that are no longer in the current server list
131
+ const keysToRemove = Array.from(existingKeys).filter((key) => !currentKeys.has(key));
132
+ keysToRemove.forEach((key) => {
133
+ const server = clientMap.get(key);
134
+ if (server?.client) {
86
135
  try {
87
- const client = await mcp_client_1.MCPClient.create(mcpServerInfo.url, mcpServerInfo.transport, mcpServerInfo.customHeaders, undefined, // no oauth support yet
88
- undefined);
89
- const connectedMcpServer = {
90
- ...mcpServerInfo,
91
- client: client,
136
+ server.client.close();
137
+ }
138
+ catch (error) {
139
+ // Avoid logging sensitive data embedded in the key (headers)
140
+ const url = server.url ?? "(unknown url)";
141
+ console.error(`Error closing MCP client for ${url}:`, error);
142
+ }
143
+ }
144
+ // Release tool ownership for this server
145
+ const owned = keyToToolsRef.current.get(key);
146
+ if (owned) {
147
+ for (const name of owned)
148
+ toolOwnerRef.current.delete(name);
149
+ keyToToolsRef.current.delete(key);
150
+ }
151
+ clientMap.delete(key);
152
+ });
153
+ // 2. Add new clients for servers that don't exist yet
154
+ const keysToAdd = Array.from(currentKeys).filter((key) => !existingKeys.has(key));
155
+ if (keysToAdd.length > 0) {
156
+ // Async initialization of new clients
157
+ Promise.allSettled(keysToAdd.map(async (key) => {
158
+ const serverInfo = currentServersMap.get(key);
159
+ try {
160
+ // Build effective handlers (per-server overrides provider)
161
+ const effectiveHandlers = {};
162
+ if (serverInfo.handlers?.elicitation) {
163
+ effectiveHandlers.elicitation = serverInfo.handlers.elicitation;
164
+ }
165
+ else if (providerElicitationHandler) {
166
+ effectiveHandlers.elicitation = async (request, extra) => await providerElicitationHandler(request, extra, serverInfo);
167
+ }
168
+ if (serverInfo.handlers?.sampling) {
169
+ effectiveHandlers.sampling = serverInfo.handlers.sampling;
170
+ }
171
+ else if (providerSamplingHandler) {
172
+ effectiveHandlers.sampling = async (request, extra) => await providerSamplingHandler(request, extra, serverInfo);
173
+ }
174
+ const client = await mcp_client_1.MCPClient.create(serverInfo.url, serverInfo.transport, serverInfo.customHeaders, undefined, undefined, effectiveHandlers);
175
+ const connectedServer = {
176
+ ...serverInfo,
177
+ client,
92
178
  };
93
- // note because the promises may resolve in any order, the resulting
94
- // array may not be in the same order as the input array
95
- setConnectedMcpServers((prev) => [
96
- // replace the server if it already exists
97
- ...prev.filter((s) => !equalsMcpServer(s, mcpServerInfo)),
98
- connectedMcpServer,
99
- ]);
100
- return connectedMcpServer;
179
+ clientMap.set(key, connectedServer);
180
+ setConnectedMcpServers(Array.from(clientMap.values()));
181
+ // Register tools from this server (deduplicated by ownership)
182
+ try {
183
+ const tools = await client.listTools();
184
+ tools.forEach((tool) => {
185
+ // Skip if another server already owns this tool
186
+ const currentOwner = toolOwnerRef.current.get(tool.name);
187
+ if (currentOwner && currentOwner !== key) {
188
+ return;
189
+ }
190
+ // Record ownership for this server key
191
+ if (!currentOwner) {
192
+ toolOwnerRef.current.set(tool.name, key);
193
+ if (!keyToToolsRef.current.has(key)) {
194
+ keyToToolsRef.current.set(key, new Set());
195
+ }
196
+ keyToToolsRef.current.get(key).add(tool.name);
197
+ }
198
+ registerTool({
199
+ description: tool.description ?? "",
200
+ name: tool.name,
201
+ tool: async (args = {}) => {
202
+ const server = clientMap.get(key);
203
+ if (!server?.client) {
204
+ throw new Error(`MCP server for tool ${tool.name} is not connected`);
205
+ }
206
+ const result = await server.client.callTool(tool.name, args);
207
+ if (result.isError) {
208
+ const errorMessage = extractErrorMessage(result.content);
209
+ throw new Error(errorMessage);
210
+ }
211
+ return result.content;
212
+ },
213
+ toolSchema: tool.inputSchema,
214
+ transformToContent: (content) => {
215
+ if ((0, content_parts_1.isContentPartArray)(content)) {
216
+ return content;
217
+ }
218
+ return [{ type: "text", text: (0, content_parts_1.toText)(content) }];
219
+ },
220
+ });
221
+ });
222
+ }
223
+ catch (error) {
224
+ console.error(`Failed to register tools from MCP server ${serverInfo.url}:`, error);
225
+ }
101
226
  }
102
227
  catch (error) {
103
- const failedMcpServer = {
104
- ...mcpServerInfo,
228
+ const failedServer = {
229
+ ...serverInfo,
105
230
  connectionError: error,
106
231
  };
107
- // note because the promises may resolve in any order, the resulting
108
- // array may not be in the same order as the input array
109
- setConnectedMcpServers((prev) => [
110
- // replace the server if it already exists
111
- ...prev.filter((s) => !equalsMcpServer(s, mcpServerInfo)),
112
- failedMcpServer,
113
- ]);
114
- return failedMcpServer;
232
+ clientMap.set(key, failedServer);
233
+ setConnectedMcpServers(Array.from(clientMap.values()));
234
+ console.error(`Failed to connect to MCP server ${serverInfo.url}:`, error);
115
235
  }
116
236
  }));
117
- // note do not rely on the state
118
- const connectedMcpServers = mcpServers
119
- .filter((result) => result.status === "fulfilled")
120
- .map((result) => result.value);
121
- // Now create a map of tool name to MCP client
122
- const serverToolLists = connectedMcpServers.map(async (mcpServer) => {
123
- const tools = (await mcpServer.client?.listTools()) ?? [];
124
- tools.forEach((tool) => {
125
- mcpServerMap.set(tool.name, mcpServer);
126
- });
127
- return tools;
128
- });
129
- const toolResults = await Promise.allSettled(serverToolLists);
130
- // Just log the failed tools, we can't do anything about them
131
- const failedTools = toolResults.filter((result) => result.status === "rejected");
132
- if (failedTools.length > 0) {
133
- console.error("Failed to register tools from MCP servers:", failedTools.map((result) => result.reason));
237
+ }
238
+ // Update state after removals (additions update state asynchronously above)
239
+ if (keysToRemove.length > 0) {
240
+ setConnectedMcpServers(Array.from(clientMap.values()));
241
+ }
242
+ // eslint-disable-next-line react-hooks/exhaustive-deps
243
+ }, [currentServersMap, registerTool]);
244
+ // Update handlers when they change (without recreating clients)
245
+ (0, react_1.useEffect)(() => {
246
+ const clientMap = clientMapRef.current;
247
+ clientMap.forEach((server, key) => {
248
+ if (!server.client) {
249
+ return; // Skip failed servers
250
+ }
251
+ const serverInfo = currentServersMap.get(key);
252
+ if (!serverInfo) {
253
+ return; // Server was removed, handled by main effect
134
254
  }
135
- // Register the successful tools
136
- const allTools = toolResults
137
- .filter((result) => result.status === "fulfilled")
138
- .map((result) => result.value)
139
- .flat();
140
- allTools.forEach((tool) => {
141
- registerTool({
142
- description: tool.description ?? "",
143
- name: tool.name,
144
- tool: async (args) => {
145
- const mcpServer = mcpServerMap.get(tool.name);
146
- if (!mcpServer) {
147
- // should never happen
148
- throw new Error(`MCP server for tool ${tool.name} not found`);
149
- }
150
- if (!mcpServer.client) {
151
- // this can't actually happen because the tool can't be registered if the server is not connected
152
- throw new Error(`MCP server for tool ${tool.name} is not connected`);
153
- }
154
- const result = await mcpServer.client.callTool(tool.name, args);
155
- if (result.isError) {
156
- const errorMessage = extractErrorMessage(result.content);
157
- throw new Error(errorMessage);
158
- }
159
- return result.content;
160
- },
161
- toolSchema: tool.inputSchema,
162
- transformToContent: (content) => {
163
- // MCP tools can return content in various formats; pass through arrays of content parts
164
- // unchanged, otherwise stringify into a text content part.
165
- if ((0, content_parts_1.isContentPartArray)(content)) {
166
- return content;
167
- }
168
- return [{ type: "text", text: (0, content_parts_1.toText)(content) }];
169
- },
170
- });
255
+ // Determine effective handlers
256
+ const effectiveElicitationHandler = serverInfo.handlers?.elicitation ??
257
+ (providerElicitationHandler
258
+ ? async (request, extra) => await providerElicitationHandler(request, extra, serverInfo)
259
+ : undefined);
260
+ const effectiveSamplingHandler = serverInfo.handlers?.sampling ??
261
+ (providerSamplingHandler
262
+ ? async (request, extra) => await providerSamplingHandler(request, extra, serverInfo)
263
+ : undefined);
264
+ // Update handlers unconditionally (allows removal by passing undefined)
265
+ server.client.updateElicitationHandler?.(effectiveElicitationHandler);
266
+ server.client.updateSamplingHandler?.(effectiveSamplingHandler);
267
+ });
268
+ }, [currentServersMap, providerElicitationHandler, providerSamplingHandler]);
269
+ // Cleanup on unmount: close all clients
270
+ (0, react_1.useEffect)(() => {
271
+ const clientMap = clientMapRef.current;
272
+ const ownerMapAtMount = toolOwnerRef.current;
273
+ const keyToToolsAtMount = keyToToolsRef.current;
274
+ return () => {
275
+ clientMap.forEach((server, _key) => {
276
+ if (server.client) {
277
+ try {
278
+ server.client.close();
279
+ }
280
+ catch (error) {
281
+ const url = server.url ?? "(unknown url)";
282
+ console.error(`Error closing MCP client on unmount for ${url}:`, error);
283
+ }
284
+ }
171
285
  });
172
- }
173
- // normalize the server infos
174
- const mcpServerInfos = mcpServers.map((mcpServer) => typeof mcpServer === "string"
175
- ? { url: mcpServer, transport: mcp_client_1.MCPTransport.SSE }
176
- : mcpServer);
177
- registerMcpServers(mcpServerInfos);
178
- }, [mcpServers, registerTool]);
179
- return (react_1.default.createElement(McpProviderContext.Provider, { value: connectedMcpServers }, children));
286
+ clientMap.clear();
287
+ ownerMapAtMount.clear();
288
+ keyToToolsAtMount.clear();
289
+ };
290
+ }, []);
291
+ const contextValue = (0, react_1.useMemo)(() => ({
292
+ servers: connectedMcpServers,
293
+ elicitation,
294
+ resolveElicitation,
295
+ }), [connectedMcpServers, elicitation, resolveElicitation]);
296
+ return (react_1.default.createElement(McpProviderContext.Provider, { value: contextValue }, children));
180
297
  };
181
298
  exports.TamboMcpProvider = TamboMcpProvider;
182
299
  /**
@@ -203,12 +320,64 @@ exports.TamboMcpProvider = TamboMcpProvider;
203
320
  * @returns The MCP servers
204
321
  */
205
322
  const useTamboMcpServers = () => {
206
- return (0, react_1.useContext)(McpProviderContext);
323
+ return (0, react_1.useContext)(McpProviderContext).servers;
207
324
  };
208
325
  exports.useTamboMcpServers = useTamboMcpServers;
209
- function equalsMcpServer(s, mcpServerInfo) {
210
- return (s.url === mcpServerInfo.url &&
211
- s.transport === mcpServerInfo.transport &&
212
- (0, fast_equals_1.deepEqual)(s.customHeaders, mcpServerInfo.customHeaders));
326
+ /**
327
+ * Hook to access elicitation context from TamboMcpProvider.
328
+ * This provides access to the current elicitation request and methods to respond to it.
329
+ *
330
+ * The elicitation state is automatically managed by TamboMcpProvider when MCP servers
331
+ * request user input through the elicitation protocol.
332
+ * @returns The elicitation context with current request and response handlers
333
+ * @example
334
+ * ```tsx
335
+ * function ElicitationUI() {
336
+ * const { elicitation, resolveElicitation } = useTamboElicitationContext();
337
+ *
338
+ * if (!elicitation) return null;
339
+ *
340
+ * return (
341
+ * <div>
342
+ * <p>{elicitation.message}</p>
343
+ * <button onClick={() => resolveElicitation?.({ action: "accept", content: {} })}>
344
+ * Accept
345
+ * </button>
346
+ * </div>
347
+ * );
348
+ * }
349
+ * ```
350
+ */
351
+ const useTamboElicitationContext = () => {
352
+ const context = (0, react_1.useContext)(McpProviderContext);
353
+ return {
354
+ elicitation: context.elicitation,
355
+ resolveElicitation: context.resolveElicitation,
356
+ };
357
+ };
358
+ exports.useTamboElicitationContext = useTamboElicitationContext;
359
+ /**
360
+ * Creates a stable identifier for an MCP server based on its connection properties.
361
+ * Two servers with the same URL, transport, and headers will have the same key.
362
+ * @returns A stable string key identifying the server
363
+ */
364
+ function getServerKey(serverInfo) {
365
+ const headerStr = serverInfo.customHeaders
366
+ ? JSON.stringify(Object.entries(serverInfo.customHeaders)
367
+ .map(([k, v]) => [k.toLowerCase(), v])
368
+ .sort(([a], [b]) => a.localeCompare(b)))
369
+ : "";
370
+ return `${serverInfo.url}|${serverInfo.transport ?? mcp_client_1.MCPTransport.SSE}|${headerStr}`;
371
+ }
372
+ /**
373
+ * Normalizes a server definition (string or object) into a McpServerInfo.
374
+ * @returns The normalized McpServerInfo object
375
+ */
376
+ function normalizeServerInfo(server) {
377
+ const s = typeof server === "string"
378
+ ? { url: server, transport: mcp_client_1.MCPTransport.SSE }
379
+ : server;
380
+ const key = getServerKey(s);
381
+ return { ...s, key };
213
382
  }
214
383
  //# sourceMappingURL=tambo-mcp-provider.js.map