@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
@@ -0,0 +1,259 @@
1
+ import { renderHook, act } from "@testing-library/react";
2
+ import { useElicitation, } from "../elicitation";
3
+ // Create a mock RequestHandlerExtra for testing
4
+ function createMockExtra() {
5
+ return {
6
+ signal: new AbortController().signal,
7
+ requestId: "test-request-id",
8
+ sendNotification: (async () => { }),
9
+ sendRequest: (async () => ({ _meta: {} })),
10
+ };
11
+ }
12
+ describe("useElicitation", () => {
13
+ it("initializes with null state", () => {
14
+ const { result } = renderHook(() => useElicitation());
15
+ expect(result.current.elicitation).toBeNull();
16
+ expect(result.current.resolveElicitation).toBeNull();
17
+ });
18
+ it("provides state setters", () => {
19
+ const { result } = renderHook(() => useElicitation());
20
+ expect(typeof result.current.setElicitation).toBe("function");
21
+ expect(typeof result.current.setResolveElicitation).toBe("function");
22
+ });
23
+ it("provides a default elicitation handler", () => {
24
+ const { result } = renderHook(() => useElicitation());
25
+ expect(typeof result.current.defaultElicitationHandler).toBe("function");
26
+ });
27
+ describe("defaultElicitationHandler", () => {
28
+ it("sets elicitation state when called", async () => {
29
+ const { result } = renderHook(() => useElicitation());
30
+ const request = {
31
+ params: {
32
+ message: "Please provide your name",
33
+ requestedSchema: {
34
+ type: "object",
35
+ properties: {
36
+ name: { type: "string", description: "Your name" },
37
+ },
38
+ required: ["name"],
39
+ },
40
+ },
41
+ };
42
+ // Start the handler but don't await yet
43
+ let handlerPromise;
44
+ const extra = createMockExtra();
45
+ act(() => {
46
+ handlerPromise = result.current.defaultElicitationHandler(request, extra);
47
+ });
48
+ // Elicitation should be set
49
+ expect(result.current.elicitation).toEqual({
50
+ message: "Please provide your name",
51
+ requestedSchema: request.params.requestedSchema,
52
+ signal: extra.signal,
53
+ });
54
+ // Resolve callback should be set
55
+ expect(result.current.resolveElicitation).not.toBeNull();
56
+ // Clean up by resolving
57
+ act(() => {
58
+ result.current.resolveElicitation?.({
59
+ action: "cancel",
60
+ });
61
+ });
62
+ await handlerPromise;
63
+ });
64
+ it("resolves promise when resolveElicitation is called with accept", async () => {
65
+ const { result } = renderHook(() => useElicitation());
66
+ const request = {
67
+ params: {
68
+ message: "Enter your email",
69
+ requestedSchema: {
70
+ type: "object",
71
+ properties: {
72
+ email: {
73
+ type: "string",
74
+ format: "email",
75
+ description: "Email address",
76
+ },
77
+ },
78
+ },
79
+ },
80
+ };
81
+ // Start the handler
82
+ let handlerPromise;
83
+ const extra = createMockExtra();
84
+ act(() => {
85
+ handlerPromise = result.current.defaultElicitationHandler(request, extra);
86
+ });
87
+ // Resolve with accept
88
+ const response = {
89
+ action: "accept",
90
+ content: { email: "test@example.com" },
91
+ };
92
+ act(() => {
93
+ result.current.resolveElicitation?.(response);
94
+ });
95
+ // Wait for promise to resolve
96
+ const resolvedValue = await handlerPromise;
97
+ expect(resolvedValue).toEqual(response);
98
+ });
99
+ it("resolves promise when resolveElicitation is called with decline", async () => {
100
+ const { result } = renderHook(() => useElicitation());
101
+ const request = {
102
+ params: {
103
+ message: "Provide input",
104
+ requestedSchema: {
105
+ type: "object",
106
+ properties: {
107
+ value: { type: "string" },
108
+ },
109
+ },
110
+ },
111
+ };
112
+ let handlerPromise;
113
+ const extra = createMockExtra();
114
+ act(() => {
115
+ handlerPromise = result.current.defaultElicitationHandler(request, extra);
116
+ });
117
+ const response = {
118
+ action: "decline",
119
+ };
120
+ act(() => {
121
+ result.current.resolveElicitation?.(response);
122
+ });
123
+ const resolvedValue = await handlerPromise;
124
+ expect(resolvedValue).toEqual(response);
125
+ });
126
+ it("resolves promise when resolveElicitation is called with cancel", async () => {
127
+ const { result } = renderHook(() => useElicitation());
128
+ const request = {
129
+ params: {
130
+ message: "Provide input",
131
+ requestedSchema: {
132
+ type: "object",
133
+ properties: {
134
+ value: { type: "string" },
135
+ },
136
+ },
137
+ },
138
+ };
139
+ let handlerPromise;
140
+ const extra = createMockExtra();
141
+ act(() => {
142
+ handlerPromise = result.current.defaultElicitationHandler(request, extra);
143
+ });
144
+ const response = {
145
+ action: "cancel",
146
+ };
147
+ act(() => {
148
+ result.current.resolveElicitation?.(response);
149
+ });
150
+ const resolvedValue = await handlerPromise;
151
+ expect(resolvedValue).toEqual(response);
152
+ });
153
+ it("handles multiple sequential elicitations", async () => {
154
+ const { result } = renderHook(() => useElicitation());
155
+ // First elicitation
156
+ const request1 = {
157
+ params: {
158
+ message: "First request",
159
+ requestedSchema: {
160
+ type: "object",
161
+ properties: {
162
+ field1: { type: "string" },
163
+ },
164
+ },
165
+ },
166
+ };
167
+ let promise1;
168
+ const extra1 = createMockExtra();
169
+ act(() => {
170
+ promise1 = result.current.defaultElicitationHandler(request1, extra1);
171
+ });
172
+ expect(result.current.elicitation?.message).toBe("First request");
173
+ act(() => {
174
+ result.current.resolveElicitation?.({
175
+ action: "accept",
176
+ content: { field1: "value1" },
177
+ });
178
+ });
179
+ const result1 = await promise1;
180
+ expect(result1).toEqual({
181
+ action: "accept",
182
+ content: { field1: "value1" },
183
+ });
184
+ // Second elicitation
185
+ const request2 = {
186
+ params: {
187
+ message: "Second request",
188
+ requestedSchema: {
189
+ type: "object",
190
+ properties: {
191
+ field2: { type: "number" },
192
+ },
193
+ },
194
+ },
195
+ };
196
+ let promise2;
197
+ const extra2 = createMockExtra();
198
+ act(() => {
199
+ promise2 = result.current.defaultElicitationHandler(request2, extra2);
200
+ });
201
+ expect(result.current.elicitation?.message).toBe("Second request");
202
+ act(() => {
203
+ result.current.resolveElicitation?.({
204
+ action: "accept",
205
+ content: { field2: 42 },
206
+ });
207
+ });
208
+ const result2 = await promise2;
209
+ expect(result2).toEqual({
210
+ action: "accept",
211
+ content: { field2: 42 },
212
+ });
213
+ });
214
+ it("maintains stable handler reference across re-renders", () => {
215
+ const { result, rerender } = renderHook(() => useElicitation());
216
+ const firstHandler = result.current.defaultElicitationHandler;
217
+ rerender();
218
+ const secondHandler = result.current.defaultElicitationHandler;
219
+ expect(firstHandler).toBe(secondHandler);
220
+ });
221
+ });
222
+ describe("state management", () => {
223
+ it("allows manual state updates via setElicitation", () => {
224
+ const { result } = renderHook(() => useElicitation());
225
+ const customElicitation = {
226
+ message: "Custom message",
227
+ requestedSchema: {
228
+ type: "object",
229
+ properties: {
230
+ custom: { type: "boolean" },
231
+ },
232
+ },
233
+ };
234
+ act(() => {
235
+ result.current.setElicitation(customElicitation);
236
+ });
237
+ expect(result.current.elicitation).toEqual(customElicitation);
238
+ });
239
+ it("allows clearing elicitation state", () => {
240
+ const { result } = renderHook(() => useElicitation());
241
+ const elicitation = {
242
+ message: "Test",
243
+ requestedSchema: {
244
+ type: "object",
245
+ properties: {},
246
+ },
247
+ };
248
+ act(() => {
249
+ result.current.setElicitation(elicitation);
250
+ });
251
+ expect(result.current.elicitation).not.toBeNull();
252
+ act(() => {
253
+ result.current.setElicitation(null);
254
+ });
255
+ expect(result.current.elicitation).toBeNull();
256
+ });
257
+ });
258
+ });
259
+ //# sourceMappingURL=elicitation.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elicitation.test.js","sourceRoot":"","sources":["../../../src/mcp/__tests__/elicitation.test.tsx"],"names":[],"mappings":"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","sourcesContent":["import { renderHook, act } from \"@testing-library/react\";\nimport {\n useElicitation,\n type TamboElicitationRequest,\n type TamboElicitationResponse,\n} from \"../elicitation\";\nimport type { RequestHandlerExtra } from \"@modelcontextprotocol/sdk/shared/protocol.js\";\nimport type {\n ClientNotification,\n ClientRequest,\n} from \"@modelcontextprotocol/sdk/types.js\";\n\n// Create a mock RequestHandlerExtra for testing\nfunction createMockExtra(): RequestHandlerExtra<\n ClientRequest,\n ClientNotification\n> {\n return {\n signal: new AbortController().signal,\n requestId: \"test-request-id\",\n\n sendNotification: (async () => {}) as any,\n\n sendRequest: (async () => ({ _meta: {} })) as any,\n };\n}\n\ndescribe(\"useElicitation\", () => {\n it(\"initializes with null state\", () => {\n const { result } = renderHook(() => useElicitation());\n\n expect(result.current.elicitation).toBeNull();\n expect(result.current.resolveElicitation).toBeNull();\n });\n\n it(\"provides state setters\", () => {\n const { result } = renderHook(() => useElicitation());\n\n expect(typeof result.current.setElicitation).toBe(\"function\");\n expect(typeof result.current.setResolveElicitation).toBe(\"function\");\n });\n\n it(\"provides a default elicitation handler\", () => {\n const { result } = renderHook(() => useElicitation());\n\n expect(typeof result.current.defaultElicitationHandler).toBe(\"function\");\n });\n\n describe(\"defaultElicitationHandler\", () => {\n it(\"sets elicitation state when called\", async () => {\n const { result } = renderHook(() => useElicitation());\n\n const request: Parameters<\n typeof result.current.defaultElicitationHandler\n >[0] = {\n params: {\n message: \"Please provide your name\",\n requestedSchema: {\n type: \"object\",\n properties: {\n name: { type: \"string\", description: \"Your name\" },\n },\n required: [\"name\"],\n },\n },\n };\n\n // Start the handler but don't await yet\n let handlerPromise: Promise<TamboElicitationResponse>;\n const extra = createMockExtra();\n act(() => {\n handlerPromise = result.current.defaultElicitationHandler(\n request,\n extra,\n );\n });\n\n // Elicitation should be set\n expect(result.current.elicitation).toEqual({\n message: \"Please provide your name\",\n requestedSchema: request.params.requestedSchema,\n signal: extra.signal,\n });\n\n // Resolve callback should be set\n expect(result.current.resolveElicitation).not.toBeNull();\n\n // Clean up by resolving\n act(() => {\n result.current.resolveElicitation?.({\n action: \"cancel\",\n });\n });\n\n await handlerPromise!;\n });\n\n it(\"resolves promise when resolveElicitation is called with accept\", async () => {\n const { result } = renderHook(() => useElicitation());\n\n const request: Parameters<\n typeof result.current.defaultElicitationHandler\n >[0] = {\n params: {\n message: \"Enter your email\",\n requestedSchema: {\n type: \"object\",\n properties: {\n email: {\n type: \"string\",\n format: \"email\",\n description: \"Email address\",\n },\n },\n },\n },\n };\n\n // Start the handler\n let handlerPromise: Promise<TamboElicitationResponse>;\n const extra = createMockExtra();\n act(() => {\n handlerPromise = result.current.defaultElicitationHandler(\n request,\n extra,\n );\n });\n\n // Resolve with accept\n const response: TamboElicitationResponse = {\n action: \"accept\",\n content: { email: \"test@example.com\" },\n };\n\n act(() => {\n result.current.resolveElicitation?.(response);\n });\n\n // Wait for promise to resolve\n const resolvedValue = await handlerPromise!;\n\n expect(resolvedValue).toEqual(response);\n });\n\n it(\"resolves promise when resolveElicitation is called with decline\", async () => {\n const { result } = renderHook(() => useElicitation());\n\n const request: Parameters<\n typeof result.current.defaultElicitationHandler\n >[0] = {\n params: {\n message: \"Provide input\",\n requestedSchema: {\n type: \"object\",\n properties: {\n value: { type: \"string\" },\n },\n },\n },\n };\n\n let handlerPromise: Promise<TamboElicitationResponse>;\n const extra = createMockExtra();\n act(() => {\n handlerPromise = result.current.defaultElicitationHandler(\n request,\n extra,\n );\n });\n\n const response: TamboElicitationResponse = {\n action: \"decline\",\n };\n\n act(() => {\n result.current.resolveElicitation?.(response);\n });\n\n const resolvedValue = await handlerPromise!;\n\n expect(resolvedValue).toEqual(response);\n });\n\n it(\"resolves promise when resolveElicitation is called with cancel\", async () => {\n const { result } = renderHook(() => useElicitation());\n\n const request: Parameters<\n typeof result.current.defaultElicitationHandler\n >[0] = {\n params: {\n message: \"Provide input\",\n requestedSchema: {\n type: \"object\",\n properties: {\n value: { type: \"string\" },\n },\n },\n },\n };\n\n let handlerPromise: Promise<TamboElicitationResponse>;\n const extra = createMockExtra();\n act(() => {\n handlerPromise = result.current.defaultElicitationHandler(\n request,\n extra,\n );\n });\n\n const response: TamboElicitationResponse = {\n action: \"cancel\",\n };\n\n act(() => {\n result.current.resolveElicitation?.(response);\n });\n\n const resolvedValue = await handlerPromise!;\n\n expect(resolvedValue).toEqual(response);\n });\n\n it(\"handles multiple sequential elicitations\", async () => {\n const { result } = renderHook(() => useElicitation());\n\n // First elicitation\n const request1: Parameters<\n typeof result.current.defaultElicitationHandler\n >[0] = {\n params: {\n message: \"First request\",\n requestedSchema: {\n type: \"object\",\n properties: {\n field1: { type: \"string\" },\n },\n },\n },\n };\n\n let promise1: Promise<TamboElicitationResponse>;\n const extra1 = createMockExtra();\n act(() => {\n promise1 = result.current.defaultElicitationHandler(request1, extra1);\n });\n\n expect(result.current.elicitation?.message).toBe(\"First request\");\n\n act(() => {\n result.current.resolveElicitation?.({\n action: \"accept\",\n content: { field1: \"value1\" },\n });\n });\n\n const result1 = await promise1!;\n expect(result1).toEqual({\n action: \"accept\",\n content: { field1: \"value1\" },\n });\n\n // Second elicitation\n const request2: Parameters<\n typeof result.current.defaultElicitationHandler\n >[0] = {\n params: {\n message: \"Second request\",\n requestedSchema: {\n type: \"object\",\n properties: {\n field2: { type: \"number\" },\n },\n },\n },\n };\n\n let promise2: Promise<TamboElicitationResponse>;\n const extra2 = createMockExtra();\n act(() => {\n promise2 = result.current.defaultElicitationHandler(request2, extra2);\n });\n\n expect(result.current.elicitation?.message).toBe(\"Second request\");\n\n act(() => {\n result.current.resolveElicitation?.({\n action: \"accept\",\n content: { field2: 42 },\n });\n });\n\n const result2 = await promise2!;\n expect(result2).toEqual({\n action: \"accept\",\n content: { field2: 42 },\n });\n });\n\n it(\"maintains stable handler reference across re-renders\", () => {\n const { result, rerender } = renderHook(() => useElicitation());\n\n const firstHandler = result.current.defaultElicitationHandler;\n\n rerender();\n\n const secondHandler = result.current.defaultElicitationHandler;\n\n expect(firstHandler).toBe(secondHandler);\n });\n });\n\n describe(\"state management\", () => {\n it(\"allows manual state updates via setElicitation\", () => {\n const { result } = renderHook(() => useElicitation());\n\n const customElicitation: TamboElicitationRequest = {\n message: \"Custom message\",\n requestedSchema: {\n type: \"object\",\n properties: {\n custom: { type: \"boolean\" },\n },\n },\n };\n\n act(() => {\n result.current.setElicitation(customElicitation);\n });\n\n expect(result.current.elicitation).toEqual(customElicitation);\n });\n\n it(\"allows clearing elicitation state\", () => {\n const { result } = renderHook(() => useElicitation());\n\n const elicitation: TamboElicitationRequest = {\n message: \"Test\",\n requestedSchema: {\n type: \"object\",\n properties: {},\n },\n };\n\n act(() => {\n result.current.setElicitation(elicitation);\n });\n\n expect(result.current.elicitation).not.toBeNull();\n\n act(() => {\n result.current.setElicitation(null);\n });\n\n expect(result.current.elicitation).toBeNull();\n });\n });\n});\n"]}
@@ -8,7 +8,6 @@ jest.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
8
8
  callTool: jest.fn(),
9
9
  setRequestHandler: jest.fn(),
10
10
  removeRequestHandler: jest.fn(),
11
- onclose: null,
12
11
  })),
13
12
  }));
14
13
  jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({
@@ -43,7 +42,6 @@ describe("MCPClient", () => {
43
42
  callTool: jest.fn(),
44
43
  setRequestHandler: jest.fn(),
45
44
  removeRequestHandler: jest.fn(),
46
- onclose: null,
47
45
  };
48
46
  mockTransportInstance = {
49
47
  sessionId: "test-session-id",
@@ -81,264 +79,6 @@ describe("MCPClient", () => {
81
79
  expect(MockedStreamableHTTPClientTransport).toHaveBeenCalledWith(new URL(endpoint), { sessionId: undefined, requestInit: { headers: {} } });
82
80
  });
83
81
  });
84
- describe("reconnect", () => {
85
- it("should create new transport and client instances and call connect when reconnect() is called (default behavior)", async () => {
86
- const endpoint = "https://api.example.com/mcp";
87
- const client = await MCPClient.create(endpoint, MCPTransport.HTTP, undefined, undefined, undefined);
88
- // Clear previous calls to focus on reconnect behavior
89
- jest.clearAllMocks();
90
- // Create new mock instances to verify new instances are created
91
- const newMockClientInstance = {
92
- connect: jest.fn().mockResolvedValue(undefined),
93
- close: jest.fn().mockResolvedValue(undefined),
94
- listTools: jest.fn(),
95
- callTool: jest.fn(),
96
- onclose: null,
97
- };
98
- const newMockTransportInstance = {
99
- sessionId: "new-session-id",
100
- };
101
- // Mock the constructors to return new instances
102
- MockedClient.mockImplementation(() => newMockClientInstance);
103
- MockedStreamableHTTPClientTransport.mockImplementation(() => newMockTransportInstance);
104
- await client.reconnect(); // Uses default parameters
105
- // Verify old client was closed
106
- expect(mockClientInstance.close).toHaveBeenCalled();
107
- // Verify new transport was created with preserved session ID (default behavior)
108
- expect(MockedStreamableHTTPClientTransport).toHaveBeenCalledWith(new URL(endpoint), { sessionId: "test-session-id", requestInit: { headers: {} } });
109
- // Verify new client was created
110
- expect(MockedClient).toHaveBeenCalledWith({
111
- name: "tambo-mcp-client",
112
- version: "1.0.0",
113
- }, { capabilities: {} });
114
- // Verify new client's connect was called with new transport
115
- expect(newMockClientInstance.connect).toHaveBeenCalledWith(newMockTransportInstance);
116
- });
117
- it("should reconnect without session ID for SSE transport", async () => {
118
- const endpoint = "https://api.example.com/mcp";
119
- const client = await MCPClient.create(endpoint, MCPTransport.SSE, undefined, undefined, undefined);
120
- // Clear previous calls
121
- jest.clearAllMocks();
122
- await client.reconnect();
123
- expect(mockClientInstance.close).toHaveBeenCalled();
124
- expect(MockedSSEClientTransport).toHaveBeenCalledWith(new URL(endpoint), {
125
- requestInit: { headers: {} },
126
- });
127
- expect(mockClientInstance.connect).toHaveBeenCalledWith({});
128
- });
129
- it("should handle close errors when reportErrorOnClose is true", async () => {
130
- const endpoint = "https://api.example.com/mcp";
131
- const client = await MCPClient.create(endpoint, MCPTransport.HTTP, undefined, undefined, undefined);
132
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
133
- // Make close throw an error
134
- mockClientInstance.close.mockRejectedValue(new Error("Close failed"));
135
- await client.reconnect(false, true);
136
- expect(consoleSpy).toHaveBeenCalledWith("Error closing Tambo MCP Client:", expect.any(Error));
137
- consoleSpy.mockRestore();
138
- });
139
- it("should not log close errors when reportErrorOnClose is false", async () => {
140
- const endpoint = "https://api.example.com/mcp";
141
- const client = await MCPClient.create(endpoint, MCPTransport.HTTP, undefined, undefined, undefined);
142
- const consoleSpy = jest.spyOn(console, "error").mockImplementation();
143
- // Make close throw an error
144
- mockClientInstance.close.mockRejectedValue(new Error("Close failed"));
145
- await client.reconnect(false, false);
146
- expect(consoleSpy).not.toHaveBeenCalled();
147
- consoleSpy.mockRestore();
148
- });
149
- it("should create new session when newSession is true", async () => {
150
- const endpoint = "https://api.example.com/mcp";
151
- const client = await MCPClient.create(endpoint, MCPTransport.HTTP, undefined, undefined, undefined);
152
- // Clear previous calls to focus on reconnect behavior
153
- jest.clearAllMocks();
154
- // Create new mock instances to verify new instances are created
155
- const newMockClientInstance = {
156
- connect: jest.fn().mockResolvedValue(undefined),
157
- close: jest.fn().mockResolvedValue(undefined),
158
- listTools: jest.fn(),
159
- callTool: jest.fn(),
160
- onclose: null,
161
- };
162
- const newMockTransportInstance = {
163
- sessionId: "new-session-id",
164
- };
165
- // Mock the constructors to return new instances
166
- MockedClient.mockImplementation(() => newMockClientInstance);
167
- MockedStreamableHTTPClientTransport.mockImplementation(() => newMockTransportInstance);
168
- await client.reconnect(true, true); // newSession = true, reportErrorOnClose = true
169
- // Verify old client was closed
170
- expect(mockClientInstance.close).toHaveBeenCalled();
171
- // Verify new transport was created with undefined session ID (new session)
172
- expect(MockedStreamableHTTPClientTransport).toHaveBeenCalledWith(new URL(endpoint), { sessionId: undefined, requestInit: { headers: {} } });
173
- // Verify new client was created
174
- expect(MockedClient).toHaveBeenCalledWith({
175
- name: "tambo-mcp-client",
176
- version: "1.0.0",
177
- }, { capabilities: {} });
178
- // Verify new client's connect was called with new transport
179
- expect(newMockClientInstance.connect).toHaveBeenCalledWith(newMockTransportInstance);
180
- });
181
- it("should reuse existing session when newSession is false (default)", async () => {
182
- const endpoint = "https://api.example.com/mcp";
183
- const client = await MCPClient.create(endpoint, MCPTransport.HTTP, undefined, undefined, undefined);
184
- // Clear previous calls to focus on reconnect behavior
185
- jest.clearAllMocks();
186
- // Create new mock instances to verify new instances are created
187
- const newMockClientInstance = {
188
- connect: jest.fn().mockResolvedValue(undefined),
189
- close: jest.fn().mockResolvedValue(undefined),
190
- listTools: jest.fn(),
191
- callTool: jest.fn(),
192
- onclose: null,
193
- };
194
- const newMockTransportInstance = {
195
- sessionId: "reused-session-id",
196
- };
197
- // Mock the constructors to return new instances
198
- MockedClient.mockImplementation(() => newMockClientInstance);
199
- MockedStreamableHTTPClientTransport.mockImplementation(() => newMockTransportInstance);
200
- await client.reconnect(false, true); // newSession = false, reportErrorOnClose = true
201
- // Verify old client was closed
202
- expect(mockClientInstance.close).toHaveBeenCalled();
203
- // Verify new transport was created with preserved session ID
204
- expect(MockedStreamableHTTPClientTransport).toHaveBeenCalledWith(new URL(endpoint), { sessionId: "test-session-id", requestInit: { headers: {} } });
205
- // Verify new client was created
206
- expect(MockedClient).toHaveBeenCalledWith({
207
- name: "tambo-mcp-client",
208
- version: "1.0.0",
209
- }, { capabilities: {} });
210
- // Verify new client's connect was called with new transport
211
- expect(newMockClientInstance.connect).toHaveBeenCalledWith(newMockTransportInstance);
212
- });
213
- });
214
- describe("onclose", () => {
215
- it("should reconnect MCPClient when client is closed by external means (no backoff on manual preemption)", async () => {
216
- const endpoint = "https://api.example.com/mcp";
217
- const client = await MCPClient.create(endpoint, MCPTransport.HTTP, undefined, undefined, undefined);
218
- jest.useFakeTimers();
219
- const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
220
- // Create new mock instances to verify reconnection creates new instances
221
- const newMockClientInstance = {
222
- connect: jest.fn().mockResolvedValue(undefined),
223
- close: jest.fn().mockResolvedValue(undefined),
224
- listTools: jest.fn(),
225
- callTool: jest.fn(),
226
- onclose: null,
227
- };
228
- const newMockTransportInstance = {
229
- sessionId: "reconnected-session-id",
230
- };
231
- // Mock the constructors to return new instances for reconnection
232
- MockedClient.mockImplementation(() => newMockClientInstance);
233
- MockedStreamableHTTPClientTransport.mockImplementation(() => newMockTransportInstance);
234
- // Reset counts after initial creation
235
- jest.clearAllMocks();
236
- // Trigger automatic onclose (schedules a delayed reconnect)
237
- client.onclose();
238
- // Manual reconnect should preempt the scheduled automatic attempt
239
- const reconnectPromise = client.reconnect();
240
- // No timers should be pending after manual preemption
241
- await reconnectPromise;
242
- // Verify warning message is logged
243
- expect(consoleSpy).toHaveBeenCalled();
244
- // Verify old client was closed
245
- expect(mockClientInstance.close).toHaveBeenCalled();
246
- // Verify new transport was created with preserved session ID
247
- expect(MockedStreamableHTTPClientTransport).toHaveBeenCalledWith(new URL(endpoint), { sessionId: "test-session-id", requestInit: { headers: {} } });
248
- // Verify new client was created
249
- expect(MockedClient).toHaveBeenCalledWith({
250
- name: "tambo-mcp-client",
251
- version: "1.0.0",
252
- }, { capabilities: {} });
253
- // Verify new client's connect was called with new transport
254
- expect(newMockClientInstance.connect).toHaveBeenCalledWith(newMockTransportInstance);
255
- // Ensure only a single reconnect attempt occurred
256
- expect(MockedClient).toHaveBeenCalledTimes(1);
257
- expect(newMockClientInstance.connect).toHaveBeenCalledTimes(1);
258
- consoleSpy.mockRestore();
259
- jest.useRealTimers();
260
- });
261
- });
262
- describe("reconnect re-entrancy and single-flight", () => {
263
- it("prevents re-entrant onclose during deliberate close and coalesces concurrent calls", async () => {
264
- const endpoint = "https://api.example.com/mcp";
265
- const client = await MCPClient.create(endpoint, MCPTransport.HTTP, undefined, undefined, undefined);
266
- // Simulate an implementation where closing the client would call its own onclose handler
267
- const closeImpl = jest.fn(async () => {
268
- if (typeof mockClientInstance.onclose === "function") {
269
- // would cause recursion if not detached
270
- mockClientInstance.onclose();
271
- }
272
- return;
273
- });
274
- mockClientInstance.close = closeImpl;
275
- // Prepare new instances for the reconnect
276
- const newMockClientInstance = {
277
- connect: jest.fn().mockResolvedValue(undefined),
278
- close: jest.fn().mockResolvedValue(undefined),
279
- listTools: jest.fn(),
280
- callTool: jest.fn(),
281
- onclose: null,
282
- };
283
- MockedClient.mockImplementation(() => newMockClientInstance);
284
- // Reset counts after initial creation
285
- jest.clearAllMocks();
286
- // Trigger auto onclose and manual reconnect nearly simultaneously
287
- client.onclose();
288
- await client.reconnect();
289
- // Should have detached onclose before calling close, avoiding recursion
290
- expect(closeImpl).toHaveBeenCalledTimes(1);
291
- expect(mockClientInstance.onclose).toBeUndefined();
292
- // Single-flight: only one new client/connect
293
- expect(MockedClient).toHaveBeenCalledTimes(1);
294
- expect(newMockClientInstance.connect).toHaveBeenCalledTimes(1);
295
- });
296
- });
297
- describe("backoff + jitter (automatic reconnect)", () => {
298
- it("applies jitter and resets to initial delay after a successful reconnect (manual preempt)", async () => {
299
- jest.useFakeTimers();
300
- const base = MCPClient.BACKOFF_INITIAL_MS;
301
- const ratio = MCPClient.BACKOFF_JITTER_RATIO;
302
- const min = Math.round(base * (1 - ratio));
303
- const max = Math.round(base * (1 + ratio));
304
- const setTimeoutSpy = jest.spyOn(global, "setTimeout");
305
- const randSpy = jest.spyOn(Math, "random").mockReturnValue(0.0); // extreme low jitter
306
- const endpoint = "https://api.example.com/mcp";
307
- const client = await MCPClient.create(endpoint, MCPTransport.HTTP, undefined, undefined, undefined);
308
- // Prepare one attempt that will succeed to avoid rescheduling
309
- MockedClient.mockImplementation(() => ({
310
- connect: jest.fn().mockResolvedValue(undefined),
311
- close: jest.fn().mockResolvedValue(undefined),
312
- listTools: jest.fn(),
313
- callTool: jest.fn(),
314
- onclose: null,
315
- }));
316
- // Trigger once to capture the delay
317
- client.onclose();
318
- expect(setTimeoutSpy).toHaveBeenCalled();
319
- const numericDelays = setTimeoutSpy.mock.calls
320
- .map((c) => c[1])
321
- .filter((v) => typeof v === "number");
322
- expect(numericDelays.length).toBeGreaterThan(0);
323
- const d = numericDelays[0];
324
- expect(d).toBeGreaterThanOrEqual(min);
325
- expect(d).toBeLessThanOrEqual(max);
326
- // Manual reconnect succeeds and should reset backoff attempts
327
- await client.reconnect();
328
- // Trigger onclose again and ensure we start from initial range again
329
- client.onclose();
330
- const numericDelays2 = setTimeoutSpy.mock.calls
331
- .map((c) => c[1])
332
- .filter((v) => typeof v === "number");
333
- expect(numericDelays2.length).toBeGreaterThanOrEqual(2);
334
- const dAgain = numericDelays2[1];
335
- expect(dAgain).toBeGreaterThanOrEqual(min);
336
- expect(dAgain).toBeLessThanOrEqual(max);
337
- jest.useRealTimers();
338
- setTimeoutSpy.mockRestore();
339
- randSpy.mockRestore();
340
- });
341
- });
342
82
  describe("listTools", () => {
343
83
  it("should list all tools with pagination", async () => {
344
84
  const endpoint = "https://api.example.com/mcp";
@@ -493,12 +233,6 @@ describe("MCPClient", () => {
493
233
  version: "1.0.0",
494
234
  }, { capabilities: {} });
495
235
  });
496
- it("should set onclose handler", async () => {
497
- const endpoint = "https://api.example.com/mcp";
498
- const _client = await MCPClient.create(endpoint, MCPTransport.HTTP, undefined, undefined, undefined);
499
- expect(mockClientInstance.onclose).toBeDefined();
500
- expect(typeof mockClientInstance.onclose).toBe("function");
501
- });
502
236
  });
503
237
  describe("handlers (elicitation/sampling)", () => {
504
238
  it("sets handlers on create when provided", async () => {