@tambo-ai/react 0.63.0 → 0.64.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 (39) hide show
  1. package/dist/hooks/use-tambo-voice.js.map +1 -1
  2. package/dist/mcp/__tests__/mcp-hooks.test.js +479 -16
  3. package/dist/mcp/__tests__/mcp-hooks.test.js.map +1 -1
  4. package/dist/mcp/__tests__/tambo-mcp-provider.test.js +156 -0
  5. package/dist/mcp/__tests__/tambo-mcp-provider.test.js.map +1 -1
  6. package/dist/mcp/index.d.ts +2 -1
  7. package/dist/mcp/index.d.ts.map +1 -1
  8. package/dist/mcp/index.js +3 -1
  9. package/dist/mcp/index.js.map +1 -1
  10. package/dist/mcp/mcp-hooks.d.ts +93 -3
  11. package/dist/mcp/mcp-hooks.d.ts.map +1 -1
  12. package/dist/mcp/mcp-hooks.js +111 -4
  13. package/dist/mcp/mcp-hooks.js.map +1 -1
  14. package/dist/mcp/tambo-mcp-provider.d.ts +16 -0
  15. package/dist/mcp/tambo-mcp-provider.d.ts.map +1 -1
  16. package/dist/mcp/tambo-mcp-provider.js +71 -7
  17. package/dist/mcp/tambo-mcp-provider.js.map +1 -1
  18. package/dist/providers/tambo-context-attachment-provider.js +2 -2
  19. package/dist/providers/tambo-context-attachment-provider.js.map +1 -1
  20. package/esm/hooks/use-tambo-voice.js.map +1 -1
  21. package/esm/mcp/__tests__/mcp-hooks.test.js +480 -17
  22. package/esm/mcp/__tests__/mcp-hooks.test.js.map +1 -1
  23. package/esm/mcp/__tests__/tambo-mcp-provider.test.js +156 -0
  24. package/esm/mcp/__tests__/tambo-mcp-provider.test.js.map +1 -1
  25. package/esm/mcp/index.d.ts +2 -1
  26. package/esm/mcp/index.d.ts.map +1 -1
  27. package/esm/mcp/index.js +1 -1
  28. package/esm/mcp/index.js.map +1 -1
  29. package/esm/mcp/mcp-hooks.d.ts +93 -3
  30. package/esm/mcp/mcp-hooks.d.ts.map +1 -1
  31. package/esm/mcp/mcp-hooks.js +109 -4
  32. package/esm/mcp/mcp-hooks.js.map +1 -1
  33. package/esm/mcp/tambo-mcp-provider.d.ts +16 -0
  34. package/esm/mcp/tambo-mcp-provider.d.ts.map +1 -1
  35. package/esm/mcp/tambo-mcp-provider.js +71 -7
  36. package/esm/mcp/tambo-mcp-provider.js.map +1 -1
  37. package/esm/providers/tambo-context-attachment-provider.js +2 -2
  38. package/esm/providers/tambo-context-attachment-provider.js.map +1 -1
  39. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"use-tambo-voice.js","sourceRoot":"","sources":["../../src/hooks/use-tambo-voice.tsx"],"names":[],"mappings":";;AAgBA,sCAyEC;AAzFD,iCAAyD;AACzD,+DAA6D;AAC7D,8EAAoE;AACpE,2DAAuD;AAEvD;;;;;;;;;;GAUG;AACH,SAAgB,aAAa;IAC3B,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,IAAA,gBAAQ,EAAgB,IAAI,CAAC,CAAC;IAClE,MAAM,MAAM,GAAG,IAAA,sCAAc,GAAE,CAAC;IAChC,MAAM,EACJ,MAAM,EACN,cAAc,EAAE,mBAAmB,EACnC,aAAa,EAAE,kBAAkB,EACjC,YAAY,EACZ,KAAK,EAAE,gBAAgB,GACxB,GAAG,IAAA,4CAAqB,EAAC;QACxB,KAAK,EAAE,IAAI;QACX,KAAK,EAAE,KAAK;QACZ,eAAe,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE;KACxC,CAAC,CAAC;IAEH,MAAM,WAAW,GAAG,MAAM,KAAK,WAAW,CAAC;IAE3C,MAAM,qBAAqB,GAAG,IAAA,oCAAgB,EAI5C;QACA,UAAU,EAAE,KAAK,EAAE,OAAe,EAAE,EAAE;YACpC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;YACtC,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YACxC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,SAAS,CAAC,EAAE,gBAAgB,EAAE;gBACnD,IAAI,EAAE,YAAY;aACnB,CAAC,CAAC;YAEH,OAAO,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,CAAC;QACD,SAAS,EAAE,CAAC,aAAa,EAAE,EAAE;YAC3B,aAAa,CAAC,aAAa,CAAC,CAAC;QAC/B,CAAC;KACF,CAAC,CAAC;IAEH,oEAAoE;IACpE,MAAM,gBAAgB,GACpB,MAAM,KAAK,SAAS;QACpB,YAAY;QACZ,CAAC,qBAAqB,CAAC,SAAS;QAChC,CAAC,qBAAqB,CAAC,SAAS,CAAC;IAEnC,IAAA,iBAAS,EAAC,GAAG,EAAE;QACb,IAAI,gBAAgB,EAAE,CAAC;YACrB,qBAAqB,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC,EAAE,CAAC,gBAAgB,EAAE,YAAY,EAAE,qBAAqB,CAAC,CAAC,CAAC;IAE5D,MAAM,cAAc,GAAG,IAAA,mBAAW,EAAC,GAAG,EAAE;QACtC,IAAI,WAAW;YAAE,OAAO;QAExB,0CAA0C;QAC1C,aAAa,CAAC,IAAI,CAAC,CAAC;QACpB,qBAAqB,CAAC,KAAK,EAAE,CAAC,CAAC,oCAAoC;QACnE,mBAAmB,EAAE,CAAC;IACxB,CAAC,EAAE,CAAC,WAAW,EAAE,mBAAmB,EAAE,qBAAqB,CAAC,CAAC,CAAC;IAE9D,MAAM,aAAa,GAAG,IAAA,mBAAW,EAAC,GAAG,EAAE;QACrC,IAAI,WAAW,EAAE,CAAC;YAChB,kBAAkB,EAAE,CAAC;QACvB,CAAC;IACH,CAAC,EAAE,CAAC,WAAW,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAEtC,OAAO;QACL,cAAc;QACd,aAAa;QACb,WAAW;QACX,gBAAgB,EAAE,gBAAgB,IAAI,IAAI;QAC1C,cAAc,EAAE,qBAAqB,CAAC,SAAS;QAC/C,UAAU;QACV,kBAAkB,EAAE,qBAAqB,CAAC,KAAK,EAAE,OAAO,IAAI,IAAI;KACjE,CAAC;AACJ,CAAC","sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\nimport { useReactMediaRecorder } from \"react-media-recorder\";\nimport { useTamboClient } from \"../providers/tambo-client-provider\";\nimport { useTamboMutation } from \"./react-query-hooks\";\n\n/**\n * Exposes functionality to record speech and transcribe it using the Tambo API.\n * @returns An object with:\n * - startRecording: A function to start recording audio and reset the current transcript.\n * - stopRecording: A function to stop recording audio and automatically kick off transcription.\n * - isRecording: A boolean indicating if the user is recording audio.\n * - isTranscribing: A boolean indicating if the audio is being transcribed.\n * - transcript: The transcript of the recorded audio.\n * - transcriptionError: An error message if the transcription fails.\n * - mediaAccessError: An error message if microphone access fails.\n */\nexport function useTamboVoice() {\n const [transcript, setTranscript] = useState<string | null>(null);\n const client = useTamboClient();\n const {\n status,\n startRecording: startMediaRecording,\n stopRecording: stopMediaRecording,\n mediaBlobUrl,\n error: mediaAccessError,\n } = useReactMediaRecorder({\n audio: true,\n video: false,\n blobPropertyBag: { type: \"audio/webm\" },\n });\n\n const isRecording = status === \"recording\";\n\n const transcriptionMutation = useTamboMutation<\n string,\n Error,\n string // blobUrl parameter\n >({\n mutationFn: async (blobUrl: string) => {\n const response = await fetch(blobUrl);\n const audioBlob = await response.blob();\n const file = new File([audioBlob], \"recording.webm\", {\n type: \"audio/webm\",\n });\n\n return await client.beta.audio.transcribe({ file });\n },\n onSuccess: (transcription) => {\n setTranscript(transcription);\n },\n });\n\n // Trigger transcription when recording stops and we have a blob URL\n const shouldTranscribe =\n status === \"stopped\" &&\n mediaBlobUrl &&\n !transcriptionMutation.isPending &&\n !transcriptionMutation.isSuccess;\n\n useEffect(() => {\n if (shouldTranscribe) {\n transcriptionMutation.mutate(mediaBlobUrl);\n }\n }, [shouldTranscribe, mediaBlobUrl, transcriptionMutation]);\n\n const startRecording = useCallback(() => {\n if (isRecording) return;\n\n // Reset state when starting new recording\n setTranscript(null);\n transcriptionMutation.reset(); // Clear any previous mutation state\n startMediaRecording();\n }, [isRecording, startMediaRecording, transcriptionMutation]);\n\n const stopRecording = useCallback(() => {\n if (isRecording) {\n stopMediaRecording();\n }\n }, [isRecording, stopMediaRecording]);\n\n return {\n startRecording,\n stopRecording,\n isRecording,\n mediaAccessError: mediaAccessError ?? null,\n isTranscribing: transcriptionMutation.isPending,\n transcript,\n transcriptionError: transcriptionMutation.error?.message ?? null,\n };\n}\n"]}
1
+ {"version":3,"file":"use-tambo-voice.js","sourceRoot":"","sources":["../../src/hooks/use-tambo-voice.tsx"],"names":[],"mappings":";;AAgBA,sCAyEC;AAzFD,iCAAyD;AACzD,+DAA6D;AAC7D,8EAAoE;AACpE,2DAAuD;AAEvD;;;;;;;;;;GAUG;AACH,SAAgB,aAAa;IAC3B,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,IAAA,gBAAQ,EAAgB,IAAI,CAAC,CAAC;IAClE,MAAM,MAAM,GAAG,IAAA,sCAAc,GAAE,CAAC;IAChC,MAAM,EACJ,MAAM,EACN,cAAc,EAAE,mBAAmB,EACnC,aAAa,EAAE,kBAAkB,EACjC,YAAY,EACZ,KAAK,EAAE,gBAAgB,GACxB,GAAG,IAAA,4CAAqB,EAAC;QACxB,KAAK,EAAE,IAAI;QACX,KAAK,EAAE,KAAK;QACZ,eAAe,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE;KACxC,CAAC,CAAC;IAEH,MAAM,WAAW,GAAG,MAAM,KAAK,WAAW,CAAC;IAE3C,MAAM,qBAAqB,GAAG,IAAA,oCAAgB,EAI5C;QACA,UAAU,EAAE,KAAK,EAAE,OAAe,EAAE,EAAE;YACpC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;YACtC,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YACxC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,SAAS,CAAC,EAAE,gBAAgB,EAAE;gBACnD,IAAI,EAAE,YAAY;aACnB,CAAC,CAAC;YAEH,OAAO,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,CAAC;QACD,SAAS,EAAE,CAAC,aAAqB,EAAE,EAAE;YACnC,aAAa,CAAC,aAAa,CAAC,CAAC;QAC/B,CAAC;KACF,CAAC,CAAC;IAEH,oEAAoE;IACpE,MAAM,gBAAgB,GACpB,MAAM,KAAK,SAAS;QACpB,YAAY;QACZ,CAAC,qBAAqB,CAAC,SAAS;QAChC,CAAC,qBAAqB,CAAC,SAAS,CAAC;IAEnC,IAAA,iBAAS,EAAC,GAAG,EAAE;QACb,IAAI,gBAAgB,EAAE,CAAC;YACrB,qBAAqB,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC,EAAE,CAAC,gBAAgB,EAAE,YAAY,EAAE,qBAAqB,CAAC,CAAC,CAAC;IAE5D,MAAM,cAAc,GAAG,IAAA,mBAAW,EAAC,GAAG,EAAE;QACtC,IAAI,WAAW;YAAE,OAAO;QAExB,0CAA0C;QAC1C,aAAa,CAAC,IAAI,CAAC,CAAC;QACpB,qBAAqB,CAAC,KAAK,EAAE,CAAC,CAAC,oCAAoC;QACnE,mBAAmB,EAAE,CAAC;IACxB,CAAC,EAAE,CAAC,WAAW,EAAE,mBAAmB,EAAE,qBAAqB,CAAC,CAAC,CAAC;IAE9D,MAAM,aAAa,GAAG,IAAA,mBAAW,EAAC,GAAG,EAAE;QACrC,IAAI,WAAW,EAAE,CAAC;YAChB,kBAAkB,EAAE,CAAC;QACvB,CAAC;IACH,CAAC,EAAE,CAAC,WAAW,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAEtC,OAAO;QACL,cAAc;QACd,aAAa;QACb,WAAW;QACX,gBAAgB,EAAE,gBAAgB,IAAI,IAAI;QAC1C,cAAc,EAAE,qBAAqB,CAAC,SAAS;QAC/C,UAAU;QACV,kBAAkB,EAAE,qBAAqB,CAAC,KAAK,EAAE,OAAO,IAAI,IAAI;KACjE,CAAC;AACJ,CAAC","sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\nimport { useReactMediaRecorder } from \"react-media-recorder\";\nimport { useTamboClient } from \"../providers/tambo-client-provider\";\nimport { useTamboMutation } from \"./react-query-hooks\";\n\n/**\n * Exposes functionality to record speech and transcribe it using the Tambo API.\n * @returns An object with:\n * - startRecording: A function to start recording audio and reset the current transcript.\n * - stopRecording: A function to stop recording audio and automatically kick off transcription.\n * - isRecording: A boolean indicating if the user is recording audio.\n * - isTranscribing: A boolean indicating if the audio is being transcribed.\n * - transcript: The transcript of the recorded audio.\n * - transcriptionError: An error message if the transcription fails.\n * - mediaAccessError: An error message if microphone access fails.\n */\nexport function useTamboVoice() {\n const [transcript, setTranscript] = useState<string | null>(null);\n const client = useTamboClient();\n const {\n status,\n startRecording: startMediaRecording,\n stopRecording: stopMediaRecording,\n mediaBlobUrl,\n error: mediaAccessError,\n } = useReactMediaRecorder({\n audio: true,\n video: false,\n blobPropertyBag: { type: \"audio/webm\" },\n });\n\n const isRecording = status === \"recording\";\n\n const transcriptionMutation = useTamboMutation<\n string,\n Error,\n string // blobUrl parameter\n >({\n mutationFn: async (blobUrl: string) => {\n const response = await fetch(blobUrl);\n const audioBlob = await response.blob();\n const file = new File([audioBlob], \"recording.webm\", {\n type: \"audio/webm\",\n });\n\n return await client.beta.audio.transcribe({ file });\n },\n onSuccess: (transcription: string) => {\n setTranscript(transcription);\n },\n });\n\n // Trigger transcription when recording stops and we have a blob URL\n const shouldTranscribe =\n status === \"stopped\" &&\n mediaBlobUrl &&\n !transcriptionMutation.isPending &&\n !transcriptionMutation.isSuccess;\n\n useEffect(() => {\n if (shouldTranscribe) {\n transcriptionMutation.mutate(mediaBlobUrl);\n }\n }, [shouldTranscribe, mediaBlobUrl, transcriptionMutation]);\n\n const startRecording = useCallback(() => {\n if (isRecording) return;\n\n // Reset state when starting new recording\n setTranscript(null);\n transcriptionMutation.reset(); // Clear any previous mutation state\n startMediaRecording();\n }, [isRecording, startMediaRecording, transcriptionMutation]);\n\n const stopRecording = useCallback(() => {\n if (isRecording) {\n stopMediaRecording();\n }\n }, [isRecording, stopMediaRecording]);\n\n return {\n startRecording,\n stopRecording,\n isRecording,\n mediaAccessError: mediaAccessError ?? null,\n isTranscribing: transcriptionMutation.isPending,\n transcript,\n transcriptionError: transcriptionMutation.error?.message ?? null,\n };\n}\n"]}
@@ -137,16 +137,16 @@ describe("useTamboMcpPromptList - individual server caching", () => {
137
137
  await (0, react_1.waitFor)(() => {
138
138
  expect(capturedPrompts.length).toBe(4);
139
139
  });
140
- // Verify all prompts are present
140
+ // Verify all prompts are present (with prefixes since we have 2 servers)
141
141
  const promptNames = capturedPrompts.map((p) => p.prompt.name);
142
- expect(promptNames).toContain("prompt-a1");
143
- expect(promptNames).toContain("prompt-a2");
144
- expect(promptNames).toContain("prompt-b1");
145
- expect(promptNames).toContain("prompt-b2");
142
+ expect(promptNames).toContain("server-a:prompt-a1");
143
+ expect(promptNames).toContain("server-a:prompt-a2");
144
+ expect(promptNames).toContain("server-b:prompt-b1");
145
+ expect(promptNames).toContain("server-b:prompt-b2");
146
146
  // Verify each prompt has the correct server info
147
- const promptA1 = capturedPrompts.find((p) => p.prompt.name === "prompt-a1");
147
+ const promptA1 = capturedPrompts.find((p) => p.prompt.name === "server-a:prompt-a1");
148
148
  expect(promptA1?.server.url).toBe("https://server-a.example");
149
- const promptB1 = capturedPrompts.find((p) => p.prompt.name === "prompt-b1");
149
+ const promptB1 = capturedPrompts.find((p) => p.prompt.name === "server-b:prompt-b1");
150
150
  expect(promptB1?.server.url).toBe("https://server-b.example");
151
151
  });
152
152
  it("should remove prompts when a server is removed", async () => {
@@ -223,10 +223,10 @@ describe("useTamboMcpPromptList - individual server caching", () => {
223
223
  expect(capturedPrompts.length).toBe(4);
224
224
  });
225
225
  const initialPromptNames = capturedPrompts.map((p) => p.prompt.name);
226
- expect(initialPromptNames).toContain("prompt-a1");
227
- expect(initialPromptNames).toContain("prompt-a2");
228
- expect(initialPromptNames).toContain("prompt-b1");
229
- expect(initialPromptNames).toContain("prompt-b2");
226
+ expect(initialPromptNames).toContain("server-a:prompt-a1");
227
+ expect(initialPromptNames).toContain("server-a:prompt-a2");
228
+ expect(initialPromptNames).toContain("server-b:prompt-b1");
229
+ expect(initialPromptNames).toContain("server-b:prompt-b2");
230
230
  // Remove server B
231
231
  rerender(react_2.default.createElement(tambo_client_provider_1.TamboClientContext.Provider, { value: {
232
232
  client: { baseURL: "https://api.tambo.co" },
@@ -243,6 +243,7 @@ describe("useTamboMcpPromptList - individual server caching", () => {
243
243
  ] },
244
244
  react_2.default.createElement(Capture, null))))));
245
245
  // Wait for prompts to be updated (server B prompts should disappear)
246
+ // When only 1 server remains, prompts should NOT be prefixed
246
247
  await (0, react_1.waitFor)(() => {
247
248
  expect(capturedPrompts.length).toBe(2);
248
249
  });
@@ -251,6 +252,7 @@ describe("useTamboMcpPromptList - individual server caching", () => {
251
252
  expect(updatedPromptNames).toContain("prompt-a2");
252
253
  expect(updatedPromptNames).not.toContain("prompt-b1");
253
254
  expect(updatedPromptNames).not.toContain("prompt-b2");
255
+ expect(updatedPromptNames).not.toContain("server-a:prompt-a1"); // No prefix when only 1 server
254
256
  // Verify server B's client was closed
255
257
  expect(clientB.close).toHaveBeenCalled();
256
258
  });
@@ -405,10 +407,10 @@ describe("useTamboMcpPromptList - individual server caching", () => {
405
407
  expect(capturedPrompts.length).toBe(1);
406
408
  expect(mcpServersCount).toBe(2); // Both servers should be in the list
407
409
  });
408
- // Verify only server A's prompts are present
410
+ // Verify only server A's prompts are present (with prefix since 2 servers configured)
409
411
  const promptNames = capturedPrompts.map((p) => p.prompt.name);
410
- expect(promptNames).toContain("prompt-a");
411
- expect(promptNames).not.toContain("prompt-b");
412
+ expect(promptNames).toContain("server-a:prompt-a");
413
+ expect(promptNames).not.toContain("server-b:prompt-b");
412
414
  });
413
415
  it("should add prompts when a new server is added", async () => {
414
416
  const serverAPrompts = {
@@ -493,12 +495,473 @@ describe("useTamboMcpPromptList - individual server caching", () => {
493
495
  ] },
494
496
  react_2.default.createElement(Capture, null))))));
495
497
  // Wait for server B prompts to be added
498
+ // Now with 2 servers, prompts should be prefixed
496
499
  await (0, react_1.waitFor)(() => {
497
500
  expect(capturedPrompts.length).toBe(2);
498
501
  });
499
502
  const promptNames = capturedPrompts.map((p) => p.prompt.name);
500
- expect(promptNames).toContain("prompt-a");
501
- expect(promptNames).toContain("prompt-b");
503
+ expect(promptNames).toContain("server-a:prompt-a");
504
+ expect(promptNames).toContain("server-b:prompt-b");
505
+ });
506
+ });
507
+ describe("useTamboMcpResourceList - resource management", () => {
508
+ let queryClient;
509
+ beforeEach(() => {
510
+ createImpl = jest.fn();
511
+ queryClient = new react_query_1.QueryClient({
512
+ defaultOptions: {
513
+ queries: {
514
+ retry: false,
515
+ },
516
+ },
517
+ });
518
+ });
519
+ afterEach(() => {
520
+ queryClient.clear();
521
+ });
522
+ it("should fetch and combine resources from multiple servers", async () => {
523
+ // Mock two servers with different resources
524
+ const serverAResources = {
525
+ resources: [
526
+ {
527
+ uri: "file:///home/user/doc1.txt",
528
+ name: "Document 1",
529
+ mimeType: "text/plain",
530
+ },
531
+ {
532
+ uri: "file:///home/user/doc2.txt",
533
+ name: "Document 2",
534
+ mimeType: "text/plain",
535
+ },
536
+ ],
537
+ };
538
+ const serverBResources = {
539
+ resources: [
540
+ {
541
+ uri: "file:///workspace/code.js",
542
+ name: "Code File",
543
+ mimeType: "text/javascript",
544
+ },
545
+ {
546
+ uri: "file:///workspace/README.md",
547
+ name: "Readme",
548
+ mimeType: "text/markdown",
549
+ },
550
+ ],
551
+ };
552
+ const mockClientA = {
553
+ listTools: jest.fn().mockResolvedValue([]),
554
+ listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
555
+ listResources: jest.fn().mockResolvedValue(serverAResources),
556
+ close: jest.fn(),
557
+ };
558
+ const mockClientB = {
559
+ listTools: jest.fn().mockResolvedValue([]),
560
+ listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
561
+ listResources: jest.fn().mockResolvedValue(serverBResources),
562
+ close: jest.fn(),
563
+ };
564
+ const clientA = {
565
+ client: mockClientA,
566
+ listTools: jest.fn().mockResolvedValue([]),
567
+ close: jest.fn(),
568
+ };
569
+ const clientB = {
570
+ client: mockClientB,
571
+ listTools: jest.fn().mockResolvedValue([]),
572
+ close: jest.fn(),
573
+ };
574
+ createImpl.mockImplementation(async (url) => {
575
+ if (url === "https://server-a.example")
576
+ return clientA;
577
+ if (url === "https://server-b.example")
578
+ return clientB;
579
+ throw new Error(`Unexpected URL: ${url}`);
580
+ });
581
+ let capturedResources = [];
582
+ const Capture = () => {
583
+ const { data: resources } = (0, mcp_hooks_1.useTamboMcpResourceList)();
584
+ (0, react_2.useEffect)(() => {
585
+ if (resources) {
586
+ capturedResources = resources;
587
+ }
588
+ }, [resources]);
589
+ return null;
590
+ };
591
+ (0, react_1.render)(react_2.default.createElement(tambo_client_provider_1.TamboClientContext.Provider, { value: {
592
+ client: { baseURL: "https://api.tambo.co" },
593
+ queryClient,
594
+ isUpdatingToken: false,
595
+ } },
596
+ react_2.default.createElement(tambo_registry_provider_1.TamboRegistryProvider, null,
597
+ react_2.default.createElement(tambo_mcp_token_provider_1.TamboMcpTokenProvider, null,
598
+ react_2.default.createElement(tambo_mcp_provider_1.TamboMcpProvider, { mcpServers: [
599
+ {
600
+ url: "https://server-a.example",
601
+ transport: mcp_client_1.MCPTransport.SSE,
602
+ },
603
+ {
604
+ url: "https://server-b.example",
605
+ transport: mcp_client_1.MCPTransport.SSE,
606
+ },
607
+ ] },
608
+ react_2.default.createElement(Capture, null))))));
609
+ // Wait for all resources to be loaded
610
+ await (0, react_1.waitFor)(() => {
611
+ expect(capturedResources.length).toBe(4);
612
+ });
613
+ // Verify all resources are present (with prefixes since we have 2 servers)
614
+ const resourceUris = capturedResources.map((r) => r.resource.uri);
615
+ expect(resourceUris).toContain("server-a:file:///home/user/doc1.txt");
616
+ expect(resourceUris).toContain("server-a:file:///home/user/doc2.txt");
617
+ expect(resourceUris).toContain("server-b:file:///workspace/code.js");
618
+ expect(resourceUris).toContain("server-b:file:///workspace/README.md");
619
+ // Verify each resource has the correct server info
620
+ const resource1 = capturedResources.find((r) => r.resource.uri === "server-a:file:///home/user/doc1.txt");
621
+ expect(resource1?.server.url).toBe("https://server-a.example");
622
+ const resource2 = capturedResources.find((r) => r.resource.uri === "server-b:file:///workspace/code.js");
623
+ expect(resource2?.server.url).toBe("https://server-b.example");
624
+ });
625
+ it("should not prefix resources when only one server exists", async () => {
626
+ const serverAResources = {
627
+ resources: [
628
+ {
629
+ uri: "file:///home/user/doc.txt",
630
+ name: "Document",
631
+ mimeType: "text/plain",
632
+ },
633
+ ],
634
+ };
635
+ const mockClientA = {
636
+ listTools: jest.fn().mockResolvedValue([]),
637
+ listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
638
+ listResources: jest.fn().mockResolvedValue(serverAResources),
639
+ close: jest.fn(),
640
+ };
641
+ const clientA = {
642
+ client: mockClientA,
643
+ listTools: jest.fn().mockResolvedValue([]),
644
+ close: jest.fn(),
645
+ };
646
+ createImpl.mockImplementation(async () => clientA);
647
+ let capturedResources = [];
648
+ const Capture = () => {
649
+ const { data: resources } = (0, mcp_hooks_1.useTamboMcpResourceList)();
650
+ (0, react_2.useEffect)(() => {
651
+ if (resources) {
652
+ capturedResources = resources;
653
+ }
654
+ }, [resources]);
655
+ return null;
656
+ };
657
+ (0, react_1.render)(react_2.default.createElement(tambo_client_provider_1.TamboClientContext.Provider, { value: {
658
+ client: { baseURL: "https://api.tambo.co" },
659
+ queryClient,
660
+ isUpdatingToken: false,
661
+ } },
662
+ react_2.default.createElement(tambo_registry_provider_1.TamboRegistryProvider, null,
663
+ react_2.default.createElement(tambo_mcp_token_provider_1.TamboMcpTokenProvider, null,
664
+ react_2.default.createElement(tambo_mcp_provider_1.TamboMcpProvider, { mcpServers: [
665
+ {
666
+ url: "https://server-a.example",
667
+ transport: mcp_client_1.MCPTransport.SSE,
668
+ },
669
+ ] },
670
+ react_2.default.createElement(Capture, null))))));
671
+ await (0, react_1.waitFor)(() => {
672
+ expect(capturedResources.length).toBe(1);
673
+ });
674
+ // No prefix when only 1 server
675
+ expect(capturedResources[0].resource.uri).toBe("file:///home/user/doc.txt");
676
+ });
677
+ it("should remove resource prefixes when a server is removed", async () => {
678
+ const serverAResources = {
679
+ resources: [
680
+ {
681
+ uri: "file:///home/user/doc1.txt",
682
+ name: "Document 1",
683
+ mimeType: "text/plain",
684
+ },
685
+ {
686
+ uri: "file:///home/user/doc2.txt",
687
+ name: "Document 2",
688
+ mimeType: "text/plain",
689
+ },
690
+ ],
691
+ };
692
+ const serverBResources = {
693
+ resources: [
694
+ {
695
+ uri: "file:///workspace/code.js",
696
+ name: "Code File",
697
+ mimeType: "text/javascript",
698
+ },
699
+ ],
700
+ };
701
+ const mockClientA = {
702
+ listTools: jest.fn().mockResolvedValue([]),
703
+ listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
704
+ listResources: jest.fn().mockResolvedValue(serverAResources),
705
+ close: jest.fn(),
706
+ };
707
+ const mockClientB = {
708
+ listTools: jest.fn().mockResolvedValue([]),
709
+ listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
710
+ listResources: jest.fn().mockResolvedValue(serverBResources),
711
+ close: jest.fn(),
712
+ };
713
+ const clientA = {
714
+ client: mockClientA,
715
+ listTools: jest.fn().mockResolvedValue([]),
716
+ close: jest.fn(),
717
+ };
718
+ const clientB = {
719
+ client: mockClientB,
720
+ listTools: jest.fn().mockResolvedValue([]),
721
+ close: jest.fn(),
722
+ };
723
+ createImpl.mockImplementation(async (url) => {
724
+ if (url === "https://server-a.example")
725
+ return clientA;
726
+ if (url === "https://server-b.example")
727
+ return clientB;
728
+ throw new Error(`Unexpected URL: ${url}`);
729
+ });
730
+ let capturedResources = [];
731
+ const Capture = () => {
732
+ const { data: resources } = (0, mcp_hooks_1.useTamboMcpResourceList)();
733
+ (0, react_2.useEffect)(() => {
734
+ if (resources) {
735
+ capturedResources = resources;
736
+ }
737
+ }, [resources]);
738
+ return null;
739
+ };
740
+ const { rerender } = (0, react_1.render)(react_2.default.createElement(tambo_client_provider_1.TamboClientContext.Provider, { value: {
741
+ client: { baseURL: "https://api.tambo.co" },
742
+ queryClient,
743
+ isUpdatingToken: false,
744
+ } },
745
+ react_2.default.createElement(tambo_registry_provider_1.TamboRegistryProvider, null,
746
+ react_2.default.createElement(tambo_mcp_token_provider_1.TamboMcpTokenProvider, null,
747
+ react_2.default.createElement(tambo_mcp_provider_1.TamboMcpProvider, { mcpServers: [
748
+ {
749
+ url: "https://server-a.example",
750
+ transport: mcp_client_1.MCPTransport.SSE,
751
+ },
752
+ {
753
+ url: "https://server-b.example",
754
+ transport: mcp_client_1.MCPTransport.SSE,
755
+ },
756
+ ] },
757
+ react_2.default.createElement(Capture, null))))));
758
+ // Wait for all resources to be loaded (prefixed)
759
+ await (0, react_1.waitFor)(() => {
760
+ expect(capturedResources.length).toBe(3);
761
+ });
762
+ const initialUris = capturedResources.map((r) => r.resource.uri);
763
+ expect(initialUris).toContain("server-a:file:///home/user/doc1.txt");
764
+ expect(initialUris).toContain("server-b:file:///workspace/code.js");
765
+ // Now remove server B
766
+ rerender(react_2.default.createElement(tambo_client_provider_1.TamboClientContext.Provider, { value: {
767
+ client: { baseURL: "https://api.tambo.co" },
768
+ queryClient,
769
+ isUpdatingToken: false,
770
+ } },
771
+ react_2.default.createElement(tambo_registry_provider_1.TamboRegistryProvider, null,
772
+ react_2.default.createElement(tambo_mcp_token_provider_1.TamboMcpTokenProvider, null,
773
+ react_2.default.createElement(tambo_mcp_provider_1.TamboMcpProvider, { mcpServers: [
774
+ {
775
+ url: "https://server-a.example",
776
+ transport: mcp_client_1.MCPTransport.SSE,
777
+ },
778
+ ] },
779
+ react_2.default.createElement(Capture, null))))));
780
+ // Wait for server B resources to be removed and prefixes stripped
781
+ await (0, react_1.waitFor)(() => {
782
+ expect(capturedResources.length).toBe(2);
783
+ });
784
+ const updatedUris = capturedResources.map((r) => r.resource.uri);
785
+ expect(updatedUris).toContain("file:///home/user/doc1.txt"); // No prefix
786
+ expect(updatedUris).toContain("file:///home/user/doc2.txt");
787
+ expect(updatedUris).not.toContain("server-a:file:///home/user/doc1.txt"); // No prefix when only 1 server
788
+ expect(updatedUris).not.toContain("server-b:file:///workspace/code.js"); // Server B removed
789
+ });
790
+ });
791
+ describe("useTamboMcpResource - read individual resource", () => {
792
+ let queryClient;
793
+ beforeEach(() => {
794
+ createImpl = jest.fn();
795
+ queryClient = new react_query_1.QueryClient({
796
+ defaultOptions: {
797
+ queries: {
798
+ retry: false,
799
+ },
800
+ },
801
+ });
802
+ });
803
+ afterEach(() => {
804
+ queryClient.clear();
805
+ });
806
+ it("should read a resource from a single server (unprefixed)", async () => {
807
+ const serverAResources = {
808
+ resources: [
809
+ {
810
+ uri: "file:///home/user/doc.txt",
811
+ name: "Document",
812
+ mimeType: "text/plain",
813
+ },
814
+ ],
815
+ };
816
+ const resourceContents = {
817
+ contents: [
818
+ {
819
+ uri: "file:///home/user/doc.txt",
820
+ mimeType: "text/plain",
821
+ text: "Hello, world!",
822
+ },
823
+ ],
824
+ };
825
+ const mockClientA = {
826
+ listTools: jest.fn().mockResolvedValue([]),
827
+ listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
828
+ listResources: jest.fn().mockResolvedValue(serverAResources),
829
+ readResource: jest.fn().mockResolvedValue(resourceContents),
830
+ close: jest.fn(),
831
+ };
832
+ const clientA = {
833
+ client: mockClientA,
834
+ listTools: jest.fn().mockResolvedValue([]),
835
+ close: jest.fn(),
836
+ };
837
+ createImpl.mockImplementation(async () => clientA);
838
+ let capturedResourceData = null;
839
+ const Capture = () => {
840
+ const { data: resourceData } = (0, mcp_hooks_1.useTamboMcpResource)("file:///home/user/doc.txt");
841
+ (0, react_2.useEffect)(() => {
842
+ if (resourceData) {
843
+ capturedResourceData = resourceData;
844
+ }
845
+ }, [resourceData]);
846
+ return null;
847
+ };
848
+ (0, react_1.render)(react_2.default.createElement(tambo_client_provider_1.TamboClientContext.Provider, { value: {
849
+ client: { baseURL: "https://api.tambo.co" },
850
+ queryClient,
851
+ isUpdatingToken: false,
852
+ } },
853
+ react_2.default.createElement(tambo_registry_provider_1.TamboRegistryProvider, null,
854
+ react_2.default.createElement(tambo_mcp_token_provider_1.TamboMcpTokenProvider, null,
855
+ react_2.default.createElement(tambo_mcp_provider_1.TamboMcpProvider, { mcpServers: [
856
+ {
857
+ url: "https://server-a.example",
858
+ transport: mcp_client_1.MCPTransport.SSE,
859
+ },
860
+ ] },
861
+ react_2.default.createElement(Capture, null))))));
862
+ await (0, react_1.waitFor)(() => {
863
+ expect(capturedResourceData).not.toBeNull();
864
+ });
865
+ expect(capturedResourceData.contents[0].text).toBe("Hello, world!");
866
+ expect(mockClientA.readResource).toHaveBeenCalledWith({
867
+ uri: "file:///home/user/doc.txt",
868
+ });
869
+ });
870
+ it("should read a resource from multiple servers (prefixed)", async () => {
871
+ const serverAResources = {
872
+ resources: [
873
+ {
874
+ uri: "file:///home/user/doc.txt",
875
+ name: "Document",
876
+ mimeType: "text/plain",
877
+ },
878
+ ],
879
+ };
880
+ const serverBResources = {
881
+ resources: [
882
+ {
883
+ uri: "file:///workspace/code.js",
884
+ name: "Code",
885
+ mimeType: "text/javascript",
886
+ },
887
+ ],
888
+ };
889
+ const resourceContentsA = {
890
+ contents: [
891
+ {
892
+ uri: "file:///home/user/doc.txt",
893
+ mimeType: "text/plain",
894
+ text: "From server A",
895
+ },
896
+ ],
897
+ };
898
+ const mockClientA = {
899
+ listTools: jest.fn().mockResolvedValue([]),
900
+ listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
901
+ listResources: jest.fn().mockResolvedValue(serverAResources),
902
+ readResource: jest.fn().mockResolvedValue(resourceContentsA),
903
+ close: jest.fn(),
904
+ };
905
+ const mockClientB = {
906
+ listTools: jest.fn().mockResolvedValue([]),
907
+ listPrompts: jest.fn().mockResolvedValue({ prompts: [] }),
908
+ listResources: jest.fn().mockResolvedValue(serverBResources),
909
+ close: jest.fn(),
910
+ };
911
+ const clientA = {
912
+ client: mockClientA,
913
+ listTools: jest.fn().mockResolvedValue([]),
914
+ close: jest.fn(),
915
+ };
916
+ const clientB = {
917
+ client: mockClientB,
918
+ listTools: jest.fn().mockResolvedValue([]),
919
+ close: jest.fn(),
920
+ };
921
+ createImpl.mockImplementation(async (url) => {
922
+ if (url === "https://server-a.example")
923
+ return clientA;
924
+ if (url === "https://server-b.example")
925
+ return clientB;
926
+ throw new Error(`Unexpected URL: ${url}`);
927
+ });
928
+ let capturedResourceData = null;
929
+ const Capture = () => {
930
+ // Request with prefix
931
+ const { data: resourceData } = (0, mcp_hooks_1.useTamboMcpResource)("server-a:file:///home/user/doc.txt");
932
+ (0, react_2.useEffect)(() => {
933
+ if (resourceData) {
934
+ capturedResourceData = resourceData;
935
+ }
936
+ }, [resourceData]);
937
+ return null;
938
+ };
939
+ (0, react_1.render)(react_2.default.createElement(tambo_client_provider_1.TamboClientContext.Provider, { value: {
940
+ client: { baseURL: "https://api.tambo.co" },
941
+ queryClient,
942
+ isUpdatingToken: false,
943
+ } },
944
+ react_2.default.createElement(tambo_registry_provider_1.TamboRegistryProvider, null,
945
+ react_2.default.createElement(tambo_mcp_token_provider_1.TamboMcpTokenProvider, null,
946
+ react_2.default.createElement(tambo_mcp_provider_1.TamboMcpProvider, { mcpServers: [
947
+ {
948
+ url: "https://server-a.example",
949
+ transport: mcp_client_1.MCPTransport.SSE,
950
+ },
951
+ {
952
+ url: "https://server-b.example",
953
+ transport: mcp_client_1.MCPTransport.SSE,
954
+ },
955
+ ] },
956
+ react_2.default.createElement(Capture, null))))));
957
+ await (0, react_1.waitFor)(() => {
958
+ expect(capturedResourceData).not.toBeNull();
959
+ });
960
+ expect(capturedResourceData.contents[0].text).toBe("From server A");
961
+ // Verify the prefix was stripped before calling the server
962
+ expect(mockClientA.readResource).toHaveBeenCalledWith({
963
+ uri: "file:///home/user/doc.txt",
964
+ });
502
965
  });
503
966
  });
504
967
  //# sourceMappingURL=mcp-hooks.test.js.map