@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.
- package/dist/hooks/react-query-hooks.d.ts +14 -1
- package/dist/hooks/react-query-hooks.d.ts.map +1 -1
- package/dist/hooks/react-query-hooks.js +13 -0
- package/dist/hooks/react-query-hooks.js.map +1 -1
- package/dist/hooks/use-tambo-stream-status.js +1 -1
- package/dist/hooks/use-tambo-stream-status.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/__tests__/elicitation.test.d.ts +2 -0
- package/dist/mcp/__tests__/elicitation.test.d.ts.map +1 -0
- package/dist/mcp/__tests__/elicitation.test.js +261 -0
- package/dist/mcp/__tests__/elicitation.test.js.map +1 -0
- package/dist/mcp/__tests__/mcp-client.test.js +0 -266
- package/dist/mcp/__tests__/mcp-client.test.js.map +1 -1
- package/dist/mcp/__tests__/mcp-hooks.test.d.ts +2 -0
- package/dist/mcp/__tests__/mcp-hooks.test.d.ts.map +1 -0
- package/dist/mcp/__tests__/mcp-hooks.test.js +504 -0
- package/dist/mcp/__tests__/mcp-hooks.test.js.map +1 -0
- package/dist/mcp/__tests__/tambo-mcp-provider.test.js +361 -16
- package/dist/mcp/__tests__/tambo-mcp-provider.test.js.map +1 -1
- package/dist/mcp/__tests__/use-mcp-servers.test.js +34 -9
- package/dist/mcp/__tests__/use-mcp-servers.test.js.map +1 -1
- package/dist/mcp/elicitation.d.ts +80 -0
- package/dist/mcp/elicitation.d.ts.map +1 -0
- package/dist/mcp/elicitation.js +55 -0
- package/dist/mcp/elicitation.js.map +1 -0
- package/dist/mcp/index.d.ts +4 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +5 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/mcp-client.d.ts +51 -86
- package/dist/mcp/mcp-client.d.ts.map +1 -1
- package/dist/mcp/mcp-client.js +22 -159
- package/dist/mcp/mcp-client.js.map +1 -1
- package/dist/mcp/mcp-hooks.d.ts +107 -0
- package/dist/mcp/mcp-hooks.d.ts.map +1 -0
- package/dist/mcp/mcp-hooks.js +103 -0
- package/dist/mcp/mcp-hooks.js.map +1 -0
- package/dist/mcp/tambo-mcp-provider.d.ts +86 -4
- package/dist/mcp/tambo-mcp-provider.d.ts.map +1 -1
- package/dist/mcp/tambo-mcp-provider.js +275 -106
- package/dist/mcp/tambo-mcp-provider.js.map +1 -1
- package/dist/providers/__tests__/tambo-thread-provider-initial-messages.test.js +3 -1
- package/dist/providers/__tests__/tambo-thread-provider-initial-messages.test.js.map +1 -1
- package/dist/providers/__tests__/tambo-thread-provider.test.js +25 -12
- package/dist/providers/__tests__/tambo-thread-provider.test.js.map +1 -1
- package/dist/providers/tambo-interactable-provider.d.ts.map +1 -1
- package/dist/providers/tambo-interactable-provider.js +11 -4
- package/dist/providers/tambo-interactable-provider.js.map +1 -1
- package/dist/providers/tambo-mcp-token-provider.d.ts +34 -0
- package/dist/providers/tambo-mcp-token-provider.d.ts.map +1 -0
- package/dist/providers/tambo-mcp-token-provider.js +69 -0
- package/dist/providers/tambo-mcp-token-provider.js.map +1 -0
- package/dist/providers/tambo-provider.d.ts.map +1 -1
- package/dist/providers/tambo-provider.js +7 -9
- package/dist/providers/tambo-provider.js.map +1 -1
- package/dist/providers/tambo-thread-provider.d.ts.map +1 -1
- package/dist/providers/tambo-thread-provider.js +14 -0
- package/dist/providers/tambo-thread-provider.js.map +1 -1
- package/esm/hooks/react-query-hooks.d.ts +14 -1
- package/esm/hooks/react-query-hooks.d.ts.map +1 -1
- package/esm/hooks/react-query-hooks.js +13 -1
- package/esm/hooks/react-query-hooks.js.map +1 -1
- package/esm/hooks/use-tambo-stream-status.js +1 -1
- package/esm/hooks/use-tambo-stream-status.js.map +1 -1
- package/esm/index.js +2 -0
- package/esm/index.js.map +1 -1
- package/esm/mcp/__tests__/elicitation.test.d.ts +2 -0
- package/esm/mcp/__tests__/elicitation.test.d.ts.map +1 -0
- package/esm/mcp/__tests__/elicitation.test.js +259 -0
- package/esm/mcp/__tests__/elicitation.test.js.map +1 -0
- package/esm/mcp/__tests__/mcp-client.test.js +0 -266
- package/esm/mcp/__tests__/mcp-client.test.js.map +1 -1
- package/esm/mcp/__tests__/mcp-hooks.test.d.ts +2 -0
- package/esm/mcp/__tests__/mcp-hooks.test.d.ts.map +1 -0
- package/esm/mcp/__tests__/mcp-hooks.test.js +469 -0
- package/esm/mcp/__tests__/mcp-hooks.test.js.map +1 -0
- package/esm/mcp/__tests__/tambo-mcp-provider.test.js +361 -16
- package/esm/mcp/__tests__/tambo-mcp-provider.test.js.map +1 -1
- package/esm/mcp/__tests__/use-mcp-servers.test.js +34 -9
- package/esm/mcp/__tests__/use-mcp-servers.test.js.map +1 -1
- package/esm/mcp/elicitation.d.ts +80 -0
- package/esm/mcp/elicitation.d.ts.map +1 -0
- package/esm/mcp/elicitation.js +52 -0
- package/esm/mcp/elicitation.js.map +1 -0
- package/esm/mcp/index.d.ts +4 -1
- package/esm/mcp/index.d.ts.map +1 -1
- package/esm/mcp/index.js +2 -1
- package/esm/mcp/index.js.map +1 -1
- package/esm/mcp/mcp-client.d.ts +51 -86
- package/esm/mcp/mcp-client.d.ts.map +1 -1
- package/esm/mcp/mcp-client.js +22 -159
- package/esm/mcp/mcp-client.js.map +1 -1
- package/esm/mcp/mcp-hooks.d.ts +107 -0
- package/esm/mcp/mcp-hooks.d.ts.map +1 -0
- package/esm/mcp/mcp-hooks.js +99 -0
- package/esm/mcp/mcp-hooks.js.map +1 -0
- package/esm/mcp/tambo-mcp-provider.d.ts +86 -4
- package/esm/mcp/tambo-mcp-provider.d.ts.map +1 -1
- package/esm/mcp/tambo-mcp-provider.js +275 -107
- package/esm/mcp/tambo-mcp-provider.js.map +1 -1
- package/esm/providers/__tests__/tambo-thread-provider-initial-messages.test.js +3 -1
- package/esm/providers/__tests__/tambo-thread-provider-initial-messages.test.js.map +1 -1
- package/esm/providers/__tests__/tambo-thread-provider.test.js +25 -12
- package/esm/providers/__tests__/tambo-thread-provider.test.js.map +1 -1
- package/esm/providers/tambo-interactable-provider.d.ts.map +1 -1
- package/esm/providers/tambo-interactable-provider.js +11 -4
- package/esm/providers/tambo-interactable-provider.js.map +1 -1
- package/esm/providers/tambo-mcp-token-provider.d.ts +34 -0
- package/esm/providers/tambo-mcp-token-provider.d.ts.map +1 -0
- package/esm/providers/tambo-mcp-token-provider.js +31 -0
- package/esm/providers/tambo-mcp-token-provider.js.map +1 -0
- package/esm/providers/tambo-provider.d.ts.map +1 -1
- package/esm/providers/tambo-provider.js +7 -9
- package/esm/providers/tambo-provider.js.map +1 -1
- package/esm/providers/tambo-thread-provider.d.ts.map +1 -1
- package/esm/providers/tambo-thread-provider.js +14 -0
- package/esm/providers/tambo-thread-provider.js.map +1 -1
- 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":"AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EACL,cAAc,GAGf,MAAM,gBAAgB,CAAC;AAOxB,gDAAgD;AAChD,SAAS,eAAe;IAItB,OAAO;QACL,MAAM,EAAE,IAAI,eAAe,EAAE,CAAC,MAAM;QACpC,SAAS,EAAE,iBAAiB;QAE5B,gBAAgB,EAAE,CAAC,KAAK,IAAI,EAAE,GAAE,CAAC,CAAQ;QAEzC,WAAW,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAQ;KAClD,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;QAEtD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC9C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,QAAQ,EAAE,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;QAEtD,MAAM,CAAC,OAAO,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC9D,MAAM,CAAC,OAAO,MAAM,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACvE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;QAEtD,MAAM,CAAC,OAAO,MAAM,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACzC,EAAE,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;YAClD,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;YAEtD,MAAM,OAAO,GAEN;gBACL,MAAM,EAAE;oBACN,OAAO,EAAE,0BAA0B;oBACnC,eAAe,EAAE;wBACf,IAAI,EAAE,QAAQ;wBACd,UAAU,EAAE;4BACV,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE;yBACnD;wBACD,QAAQ,EAAE,CAAC,MAAM,CAAC;qBACnB;iBACF;aACF,CAAC;YAEF,wCAAwC;YACxC,IAAI,cAAiD,CAAC;YACtD,MAAM,KAAK,GAAG,eAAe,EAAE,CAAC;YAChC,GAAG,CAAC,GAAG,EAAE;gBACP,cAAc,GAAG,MAAM,CAAC,OAAO,CAAC,yBAAyB,CACvD,OAAO,EACP,KAAK,CACN,CAAC;YACJ,CAAC,CAAC,CAAC;YAEH,4BAA4B;YAC5B,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC;gBACzC,OAAO,EAAE,0BAA0B;gBACnC,eAAe,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe;gBAC/C,MAAM,EAAE,KAAK,CAAC,MAAM;aACrB,CAAC,CAAC;YAEH,iCAAiC;YACjC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YAEzD,wBAAwB;YACxB,GAAG,CAAC,GAAG,EAAE;gBACP,MAAM,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC;oBAClC,MAAM,EAAE,QAAQ;iBACjB,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,MAAM,cAAe,CAAC;QACxB,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;YAC9E,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;YAEtD,MAAM,OAAO,GAEN;gBACL,MAAM,EAAE;oBACN,OAAO,EAAE,kBAAkB;oBAC3B,eAAe,EAAE;wBACf,IAAI,EAAE,QAAQ;wBACd,UAAU,EAAE;4BACV,KAAK,EAAE;gCACL,IAAI,EAAE,QAAQ;gCACd,MAAM,EAAE,OAAO;gCACf,WAAW,EAAE,eAAe;6BAC7B;yBACF;qBACF;iBACF;aACF,CAAC;YAEF,oBAAoB;YACpB,IAAI,cAAiD,CAAC;YACtD,MAAM,KAAK,GAAG,eAAe,EAAE,CAAC;YAChC,GAAG,CAAC,GAAG,EAAE;gBACP,cAAc,GAAG,MAAM,CAAC,OAAO,CAAC,yBAAyB,CACvD,OAAO,EACP,KAAK,CACN,CAAC;YACJ,CAAC,CAAC,CAAC;YAEH,sBAAsB;YACtB,MAAM,QAAQ,GAA6B;gBACzC,MAAM,EAAE,QAAQ;gBAChB,OAAO,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE;aACvC,CAAC;YAEF,GAAG,CAAC,GAAG,EAAE;gBACP,MAAM,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,QAAQ,CAAC,CAAC;YAChD,CAAC,CAAC,CAAC;YAEH,8BAA8B;YAC9B,MAAM,aAAa,GAAG,MAAM,cAAe,CAAC;YAE5C,MAAM,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;YAC/E,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;YAEtD,MAAM,OAAO,GAEN;gBACL,MAAM,EAAE;oBACN,OAAO,EAAE,eAAe;oBACxB,eAAe,EAAE;wBACf,IAAI,EAAE,QAAQ;wBACd,UAAU,EAAE;4BACV,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;yBAC1B;qBACF;iBACF;aACF,CAAC;YAEF,IAAI,cAAiD,CAAC;YACtD,MAAM,KAAK,GAAG,eAAe,EAAE,CAAC;YAChC,GAAG,CAAC,GAAG,EAAE;gBACP,cAAc,GAAG,MAAM,CAAC,OAAO,CAAC,yBAAyB,CACvD,OAAO,EACP,KAAK,CACN,CAAC;YACJ,CAAC,CAAC,CAAC;YAEH,MAAM,QAAQ,GAA6B;gBACzC,MAAM,EAAE,SAAS;aAClB,CAAC;YAEF,GAAG,CAAC,GAAG,EAAE;gBACP,MAAM,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,QAAQ,CAAC,CAAC;YAChD,CAAC,CAAC,CAAC;YAEH,MAAM,aAAa,GAAG,MAAM,cAAe,CAAC;YAE5C,MAAM,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;YAC9E,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;YAEtD,MAAM,OAAO,GAEN;gBACL,MAAM,EAAE;oBACN,OAAO,EAAE,eAAe;oBACxB,eAAe,EAAE;wBACf,IAAI,EAAE,QAAQ;wBACd,UAAU,EAAE;4BACV,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;yBAC1B;qBACF;iBACF;aACF,CAAC;YAEF,IAAI,cAAiD,CAAC;YACtD,MAAM,KAAK,GAAG,eAAe,EAAE,CAAC;YAChC,GAAG,CAAC,GAAG,EAAE;gBACP,cAAc,GAAG,MAAM,CAAC,OAAO,CAAC,yBAAyB,CACvD,OAAO,EACP,KAAK,CACN,CAAC;YACJ,CAAC,CAAC,CAAC;YAEH,MAAM,QAAQ,GAA6B;gBACzC,MAAM,EAAE,QAAQ;aACjB,CAAC;YAEF,GAAG,CAAC,GAAG,EAAE;gBACP,MAAM,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,QAAQ,CAAC,CAAC;YAChD,CAAC,CAAC,CAAC;YAEH,MAAM,aAAa,GAAG,MAAM,cAAe,CAAC;YAE5C,MAAM,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;YACxD,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;YAEtD,oBAAoB;YACpB,MAAM,QAAQ,GAEP;gBACL,MAAM,EAAE;oBACN,OAAO,EAAE,eAAe;oBACxB,eAAe,EAAE;wBACf,IAAI,EAAE,QAAQ;wBACd,UAAU,EAAE;4BACV,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;yBAC3B;qBACF;iBACF;aACF,CAAC;YAEF,IAAI,QAA2C,CAAC;YAChD,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;YACjC,GAAG,CAAC,GAAG,EAAE;gBACP,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,yBAAyB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YACxE,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YAElE,GAAG,CAAC,GAAG,EAAE;gBACP,MAAM,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC;oBAClC,MAAM,EAAE,QAAQ;oBAChB,OAAO,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE;iBAC9B,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,MAAM,OAAO,GAAG,MAAM,QAAS,CAAC;YAChC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC;gBACtB,MAAM,EAAE,QAAQ;gBAChB,OAAO,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE;aAC9B,CAAC,CAAC;YAEH,qBAAqB;YACrB,MAAM,QAAQ,GAEP;gBACL,MAAM,EAAE;oBACN,OAAO,EAAE,gBAAgB;oBACzB,eAAe,EAAE;wBACf,IAAI,EAAE,QAAQ;wBACd,UAAU,EAAE;4BACV,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;yBAC3B;qBACF;iBACF;aACF,CAAC;YAEF,IAAI,QAA2C,CAAC;YAChD,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;YACjC,GAAG,CAAC,GAAG,EAAE;gBACP,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,yBAAyB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YACxE,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;YAEnE,GAAG,CAAC,GAAG,EAAE;gBACP,MAAM,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC;oBAClC,MAAM,EAAE,QAAQ;oBAChB,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE;iBACxB,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,MAAM,OAAO,GAAG,MAAM,QAAS,CAAC;YAChC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC;gBACtB,MAAM,EAAE,QAAQ;gBAChB,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE;aACxB,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;YAC9D,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;YAEhE,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,CAAC,yBAAyB,CAAC;YAE9D,QAAQ,EAAE,CAAC;YAEX,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC,yBAAyB,CAAC;YAE/D,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;QAChC,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;YACxD,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;YAEtD,MAAM,iBAAiB,GAA4B;gBACjD,OAAO,EAAE,gBAAgB;gBACzB,eAAe,EAAE;oBACf,IAAI,EAAE,QAAQ;oBACd,UAAU,EAAE;wBACV,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;qBAC5B;iBACF;aACF,CAAC;YAEF,GAAG,CAAC,GAAG,EAAE;gBACP,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;YACnD,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;YAC3C,MAAM,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;YAEtD,MAAM,WAAW,GAA4B;gBAC3C,OAAO,EAAE,MAAM;gBACf,eAAe,EAAE;oBACf,IAAI,EAAE,QAAQ;oBACd,UAAU,EAAE,EAAE;iBACf;aACF,CAAC;YAEF,GAAG,CAAC,GAAG,EAAE;gBACP,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;YAC7C,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YAElD,GAAG,CAAC,GAAG,EAAE;gBACP,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;YACtC,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,QAAQ,EAAE,CAAC;QAChD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","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 () => {
|