@assistant-ui/react 0.15.16 → 0.15.18
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/README.md +4 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +4 -2
- package/dist/mcp-apps/McpAppRenderer.d.ts +36 -9
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +108 -57
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppsRemoteHost.js +20 -17
- package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
- package/dist/mcp-apps/app-frame.d.ts.map +1 -1
- package/dist/mcp-apps/app-frame.js +229 -115
- package/dist/mcp-apps/app-frame.js.map +1 -1
- package/dist/mcp-apps/bridge.js +2 -3
- package/dist/mcp-apps/bridge.js.map +1 -1
- package/dist/mcp-apps/index.d.ts +3 -3
- package/dist/model-context/frame/useAssistantFrameHost.d.ts +1 -1
- package/dist/model-context/frame/useAssistantFrameHost.js +10 -11
- package/dist/model-context/frame/useAssistantFrameHost.js.map +1 -1
- package/dist/model-context/makeAssistantVisible.js +1 -1
- package/dist/model-context/makeAssistantVisible.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarCopy.js +3 -3
- package/dist/primitives/actionBar/ActionBarCopy.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js +3 -3
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +3 -3
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +3 -3
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarStopSpeaking.js +3 -3
- package/dist/primitives/actionBar/ActionBarStopSpeaking.js.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreContent.js.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreItem.js.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreRoot.js.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreSeparator.js.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreTrigger.js.map +1 -1
- package/dist/primitives/actionBarMore/scope.d.ts +2 -2
- package/dist/primitives/actionBarMore/scope.d.ts.map +1 -1
- package/dist/primitives/actionBarMore/scope.js.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalAnchor.js.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalContent.js +1 -1
- package/dist/primitives/assistantModal/AssistantModalContent.js.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalTrigger.js.map +1 -1
- package/dist/primitives/assistantModal/scope.d.ts +2 -2
- package/dist/primitives/assistantModal/scope.d.ts.map +1 -1
- package/dist/primitives/assistantModal/scope.js.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerRoot.js.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
- package/dist/primitives/composer/ComposerDictationTranscript.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +1 -3
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerInputPluginContext.d.ts +1 -2
- package/dist/primitives/composer/ComposerInputPluginContext.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInputPluginContext.js +1 -6
- package/dist/primitives/composer/ComposerInputPluginContext.js.map +1 -1
- package/dist/primitives/composer/ComposerQuote.js +5 -5
- package/dist/primitives/composer/ComposerQuote.js.map +1 -1
- package/dist/primitives/composer/ComposerRoot.js +3 -3
- package/dist/primitives/composer/ComposerRoot.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopover.d.ts +5 -0
- package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopover.js +157 -130
- package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js +46 -15
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverBack.js +3 -3
- package/dist/primitives/composer/trigger/TriggerPopoverBack.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +4 -4
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +40 -10
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverItems.js +4 -4
- package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +2 -0
- package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js +63 -61
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +2 -0
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
- package/dist/primitives/composer/trigger/detectTrigger.d.ts +21 -1
- package/dist/primitives/composer/trigger/detectTrigger.d.ts.map +1 -0
- package/dist/primitives/composer/trigger/detectTrigger.js +17 -3
- package/dist/primitives/composer/trigger/detectTrigger.js.map +1 -1
- package/dist/primitives/composer/trigger/index.d.ts +3 -1
- package/dist/primitives/composer/trigger/index.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/index.js.map +1 -1
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts +6 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts.map +1 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js +9 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js.map +1 -0
- package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts +3 -4
- package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerDetectionResource.js +15 -14
- package/dist/primitives/composer/trigger/triggerDetectionResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.js +3 -5
- package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerSelectionResource.js +18 -19
- package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
- package/dist/primitives/composer.d.ts +3 -2
- package/dist/primitives/composer.js.map +1 -1
- package/dist/primitives/error/ErrorMessage.js.map +1 -1
- package/dist/primitives/message/MessageIf.js.map +1 -1
- package/dist/primitives/message/MessageParts.js.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
- package/dist/primitives/message/MessageRoot.js +4 -4
- package/dist/primitives/message/MessageRoot.js.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.d.ts +1 -1
- package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +3 -3
- package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +3 -3
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
- package/dist/primitives/thread/ThreadIf.js.map +1 -1
- package/dist/primitives/thread/ThreadViewport.js +3 -3
- package/dist/primitives/thread/ThreadViewport.js.map +1 -1
- package/dist/primitives/thread/ThreadViewportFooter.js +3 -3
- package/dist/primitives/thread/ThreadViewportFooter.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +4 -4
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/primitives/threadList/ThreadListNew.js +3 -3
- package/dist/primitives/threadList/ThreadListNew.js.map +1 -1
- package/dist/primitives/threadListFocusGroup.d.ts +2 -1
- package/dist/primitives/threadListFocusGroup.d.ts.map +1 -1
- package/dist/primitives/threadListFocusGroup.js +2 -2
- package/dist/primitives/threadListFocusGroup.js.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemRoot.js +3 -3
- package/dist/primitives/threadListItem/ThreadListItemRoot.js.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemTrigger.js +1 -1
- package/dist/primitives/threadListItem/ThreadListItemTrigger.js.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.js.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.js.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js +1 -2
- package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js.map +1 -1
- package/dist/primitives/threadListItemMore/scope.d.ts +2 -2
- package/dist/primitives/threadListItemMore/scope.d.ts.map +1 -1
- package/dist/primitives/threadListItemMore/scope.js.map +1 -1
- package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
- package/dist/sandbox-host/SandboxHost.js +148 -72
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.js +3 -1
- package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
- package/dist/unstable/useMentionAdapter.d.ts +9 -3
- package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
- package/dist/unstable/useMentionAdapter.js +50 -43
- package/dist/unstable/useMentionAdapter.js.map +1 -1
- package/dist/unstable/useModelContextSnapshot.d.ts +34 -0
- package/dist/unstable/useModelContextSnapshot.d.ts.map +1 -0
- package/dist/unstable/useModelContextSnapshot.js +61 -0
- package/dist/unstable/useModelContextSnapshot.js.map +1 -0
- package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.js +37 -29
- package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
- package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts +14 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts.map +1 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.js +85 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.js.map +1 -0
- package/dist/unstable/webmcp/convertTools.d.ts +21 -0
- package/dist/unstable/webmcp/convertTools.d.ts.map +1 -0
- package/dist/unstable/webmcp/convertTools.js +110 -0
- package/dist/unstable/webmcp/convertTools.js.map +1 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts +21 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js +105 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -0
- package/dist/unstable/webmcp/webmcp-host.d.ts +39 -0
- package/dist/unstable/webmcp/webmcp-host.d.ts.map +1 -0
- package/dist/unstable/webmcp/webmcp-host.js +29 -0
- package/dist/unstable/webmcp/webmcp-host.js.map +1 -0
- package/dist/utils/Primitive.d.ts +6 -6
- package/dist/utils/Primitive.js +2 -2
- package/dist/utils/Primitive.js.map +1 -1
- package/dist/utils/createActionButton.js +3 -3
- package/dist/utils/createActionButton.js.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.js +1 -1
- package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
- package/dist/utils/hooks/useOnScrollToBottom.js +1 -1
- package/dist/utils/hooks/useOnScrollToBottom.js.map +1 -1
- package/dist/utils/smooth/SmoothContext.d.ts +24 -0
- package/dist/utils/smooth/SmoothContext.d.ts.map +1 -1
- package/dist/utils/smooth/useSmooth.js +1 -1
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/package.json +14 -22
- package/src/index.ts +22 -1
- package/src/mcp-apps/McpAppRenderer.test.tsx +386 -9
- package/src/mcp-apps/McpAppRenderer.tsx +171 -69
- package/src/mcp-apps/McpAppsRemoteHost.concurrent.test.tsx +215 -0
- package/src/mcp-apps/McpAppsRemoteHost.ts +28 -12
- package/src/mcp-apps/app-frame.test.tsx +349 -0
- package/src/mcp-apps/app-frame.tsx +68 -9
- package/src/mcp-apps/bridge.ts +7 -2
- package/src/mcp-apps/index.ts +6 -1
- package/src/model-context/frame/AssistantFrame.test.ts +3 -3
- package/src/model-context/frame/SPEC_AssistantFrame.md +1 -1
- package/src/model-context/frame/useAssistantFrameHost.ts +2 -2
- package/src/model-context/makeAssistantVisible.tsx +1 -1
- package/src/primitives/actionBar/ActionBarCopy.tsx +1 -1
- package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +1 -1
- package/src/primitives/actionBar/ActionBarFeedbackNegative.tsx +1 -1
- package/src/primitives/actionBar/ActionBarFeedbackPositive.tsx +1 -1
- package/src/primitives/actionBar/ActionBarStopSpeaking.tsx +1 -1
- package/src/primitives/actionBarMore/scope.ts +2 -2
- package/src/primitives/assistantModal/AssistantModalContent.tsx +1 -1
- package/src/primitives/assistantModal/scope.ts +2 -2
- package/src/primitives/composer/ComposerAttachmentDropzone.tsx +1 -1
- package/src/primitives/composer/ComposerInput.test.tsx +2 -1
- package/src/primitives/composer/ComposerInput.tsx +3 -3
- package/src/primitives/composer/ComposerInputPluginContext.tsx +0 -10
- package/src/primitives/composer/ComposerQuote.tsx +1 -1
- package/src/primitives/composer/ComposerRoot.tsx +1 -1
- package/src/primitives/composer/trigger/TriggerPopover.tsx +16 -1
- package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +4 -2
- package/src/primitives/composer/trigger/TriggerPopoverBack.tsx +1 -1
- package/src/primitives/composer/trigger/TriggerPopoverBehavior.test.tsx +109 -0
- package/src/primitives/composer/trigger/TriggerPopoverCategories.tsx +1 -1
- package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +4 -2
- package/src/primitives/composer/trigger/TriggerPopoverItems.tsx +1 -1
- package/src/primitives/composer/trigger/TriggerPopoverResource.ts +4 -1
- package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +2 -0
- package/src/primitives/composer/trigger/detectTrigger.test.ts +88 -5
- package/src/primitives/composer/trigger/detectTrigger.ts +50 -9
- package/src/primitives/composer/trigger/index.ts +1 -0
- package/src/primitives/composer/trigger/matchesTriggerItemQuery.test.ts +52 -0
- package/src/primitives/composer/trigger/matchesTriggerItemQuery.ts +13 -0
- package/src/primitives/composer/trigger/triggerDetectionResource.ts +10 -7
- package/src/primitives/composer/trigger/triggerNavigationResource.ts +3 -10
- package/src/primitives/composer/trigger/triggerSelectionResource.test.ts +15 -3
- package/src/primitives/composer/trigger/triggerSelectionResource.ts +1 -4
- package/src/primitives/composer.ts +1 -0
- package/src/primitives/message/MessageRoot.tsx +1 -1
- package/src/primitives/selectionToolbar/SelectionToolbarQuote.tsx +1 -1
- package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +1 -1
- package/src/primitives/thread/ThreadViewport.tsx +1 -1
- package/src/primitives/thread/ThreadViewportFooter.tsx +1 -1
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +8 -7
- package/src/primitives/threadList/ThreadListNew.tsx +1 -1
- package/src/primitives/threadListFocusGroup.ts +2 -2
- package/src/primitives/threadListItem/ThreadListItemRoot.tsx +1 -1
- package/src/primitives/threadListItem/ThreadListItemTrigger.tsx +1 -1
- package/src/primitives/threadListItemMore/ThreadListItemMoreContent.tsx +1 -1
- package/src/primitives/threadListItemMore/ThreadListItemMoreRoot.tsx +1 -1
- package/src/primitives/threadListItemMore/ThreadListItemMoreTrigger.tsx +2 -2
- package/src/primitives/threadListItemMore/scope.ts +2 -2
- package/src/sandbox-host/SandboxHost.test.tsx +165 -3
- package/src/sandbox-host/SandboxHost.tsx +42 -9
- package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +31 -1
- package/src/unstable/useComposerInputHistory.test.tsx +2 -1
- package/src/unstable/useLiveCompletionAdapter.test.tsx +60 -1
- package/src/unstable/useLiveCompletionAdapter.ts +4 -1
- package/src/unstable/useMentionAdapter.test.tsx +337 -0
- package/src/unstable/useMentionAdapter.ts +94 -57
- package/src/unstable/useModelContextSnapshot.test.tsx +132 -0
- package/src/unstable/useModelContextSnapshot.ts +67 -0
- package/src/unstable/useSlashCommandAdapter.test.tsx +141 -0
- package/src/unstable/useSlashCommandAdapter.ts +47 -19
- package/src/unstable/webmcp/WebMcpRegistrationResource.ts +92 -0
- package/src/unstable/webmcp/__tests__/webmcp.fake.tsx +168 -0
- package/src/unstable/webmcp/convertTools.test.ts +393 -0
- package/src/unstable/webmcp/convertTools.ts +180 -0
- package/src/unstable/webmcp/useWebMcpProvider.test.tsx +466 -0
- package/src/unstable/webmcp/useWebMcpProvider.ts +150 -0
- package/src/unstable/webmcp/webmcp-host.test.ts +142 -0
- package/src/unstable/webmcp/webmcp-host.ts +83 -0
- package/src/utils/Primitive.tsx +2 -2
- package/src/utils/createActionButton.tsx +1 -1
- package/src/utils/hooks/useOnResizeContent.ts +1 -1
- package/src/utils/hooks/useOnScrollToBottom.ts +1 -1
- package/src/utils/smooth/useSmooth.ts +1 -1
- package/dist/legacy-runtime/runtime/MessagePartRuntime.d.ts +0 -3
- package/dist/legacy-runtime/runtime/MessagePartRuntime.js +0 -2
- package/dist/legacy-runtime/runtime/MessageRuntime.d.ts +0 -3
- package/dist/legacy-runtime/runtime/MessageRuntime.js +0 -2
- package/dist/legacy-runtime/runtime/ThreadListRuntime.d.ts +0 -3
- package/dist/legacy-runtime/runtime/ThreadListRuntime.js +0 -2
- package/dist/utils/invokeCallbackSafely.d.ts +0 -5
- package/dist/utils/invokeCallbackSafely.d.ts.map +0 -1
- package/dist/utils/invokeCallbackSafely.js +0 -15
- package/dist/utils/invokeCallbackSafely.js.map +0 -1
- package/dist/utils/useToolArgsFieldStatus.d.ts +0 -15
- package/dist/utils/useToolArgsFieldStatus.d.ts.map +0 -1
- package/dist/utils/useToolArgsFieldStatus.js +0 -23
- package/dist/utils/useToolArgsFieldStatus.js.map +0 -1
- package/src/legacy-runtime/runtime/MessagePartRuntime.ts +0 -2
- package/src/legacy-runtime/runtime/MessageRuntime.ts +0 -3
- package/src/legacy-runtime/runtime/ThreadListRuntime.ts +0 -4
- package/src/utils/invokeCallbackSafely.ts +0 -11
- package/src/utils/useToolArgsFieldStatus.ts +0 -18
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useEffect, type ReactNode } from "react";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import { expect, vi } from "vitest";
|
|
4
|
+
import { AuiConfig, AuiProvider, useAui } from "@assistant-ui/store";
|
|
5
|
+
import { ModelContext } from "@assistant-ui/core/store";
|
|
6
|
+
import type { ModelContextProvider } from "@assistant-ui/core";
|
|
7
|
+
import type { Tool } from "assistant-stream";
|
|
8
|
+
import type {
|
|
9
|
+
WebMcpHost,
|
|
10
|
+
WebMcpModelContext,
|
|
11
|
+
WebMcpToolDescriptor,
|
|
12
|
+
} from "../webmcp-host";
|
|
13
|
+
import {
|
|
14
|
+
unstable_useWebMcpProvider,
|
|
15
|
+
type Unstable_WebMcpProviderOptions,
|
|
16
|
+
type Unstable_WebMcpProviderResult,
|
|
17
|
+
} from "../useWebMcpProvider";
|
|
18
|
+
|
|
19
|
+
export type FakeWebMcpHost = WebMcpHost & {
|
|
20
|
+
registry: Map<string, WebMcpToolDescriptor>;
|
|
21
|
+
registerCalls: string[];
|
|
22
|
+
unregisterCalls: string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const createFakeWebMcpHost = (): FakeWebMcpHost => {
|
|
26
|
+
const registry = new Map<string, WebMcpToolDescriptor>();
|
|
27
|
+
const registerCalls: string[] = [];
|
|
28
|
+
const unregisterCalls: string[] = [];
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
available: true,
|
|
32
|
+
registry,
|
|
33
|
+
registerCalls,
|
|
34
|
+
unregisterCalls,
|
|
35
|
+
registerTool: (def) => {
|
|
36
|
+
if (registry.has(def.name)) {
|
|
37
|
+
throw new Error(`Tool "${def.name}" is already registered`);
|
|
38
|
+
}
|
|
39
|
+
registry.set(def.name, def);
|
|
40
|
+
registerCalls.push(def.name);
|
|
41
|
+
let disposed = false;
|
|
42
|
+
return () => {
|
|
43
|
+
if (disposed) return;
|
|
44
|
+
disposed = true;
|
|
45
|
+
if (registry.get(def.name) === def) registry.delete(def.name);
|
|
46
|
+
unregisterCalls.push(def.name);
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Mirrors the spec's registerTool: the tool enters the map synchronously while
|
|
53
|
+
// the promise resolves on a later task, aborting removes it and rejects, and a
|
|
54
|
+
// name already in the map is refused.
|
|
55
|
+
export const createAsyncModelContext = (): Map<
|
|
56
|
+
string,
|
|
57
|
+
WebMcpToolDescriptor
|
|
58
|
+
> => {
|
|
59
|
+
const registry = new Map<string, WebMcpToolDescriptor>();
|
|
60
|
+
const context: WebMcpModelContext = {
|
|
61
|
+
registerTool: (tool, options) => {
|
|
62
|
+
if (registry.has(tool.name)) {
|
|
63
|
+
return Promise.reject(
|
|
64
|
+
new DOMException(
|
|
65
|
+
`Tool "${tool.name}" is already registered`,
|
|
66
|
+
"InvalidStateError",
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
registry.set(tool.name, tool);
|
|
71
|
+
return new Promise<void>((resolve, reject) => {
|
|
72
|
+
options?.signal?.addEventListener("abort", () => {
|
|
73
|
+
registry.delete(tool.name);
|
|
74
|
+
reject(options.signal?.reason);
|
|
75
|
+
});
|
|
76
|
+
setTimeout(() => resolve(), 0);
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
(document as { modelContext?: WebMcpModelContext }).modelContext = context;
|
|
81
|
+
return registry;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const frontendTool = (
|
|
85
|
+
overrides: Partial<Tool<any, any>> = {},
|
|
86
|
+
): Tool<any, any> =>
|
|
87
|
+
({
|
|
88
|
+
type: "frontend",
|
|
89
|
+
description: "search things",
|
|
90
|
+
parameters: { type: "object", properties: {} },
|
|
91
|
+
execute: async () => "found",
|
|
92
|
+
...overrides,
|
|
93
|
+
}) as Tool<any, any>;
|
|
94
|
+
|
|
95
|
+
export type FakeProvider = ModelContextProvider & {
|
|
96
|
+
setTools: (next: Record<string, Tool<any, any>>) => void;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const createProvider = (
|
|
100
|
+
initialTools: Record<string, Tool<any, any>>,
|
|
101
|
+
): FakeProvider => {
|
|
102
|
+
let tools = initialTools;
|
|
103
|
+
const listeners = new Set<() => void>();
|
|
104
|
+
return {
|
|
105
|
+
getModelContext: () => ({ tools }),
|
|
106
|
+
subscribe: (callback: () => void) => {
|
|
107
|
+
listeners.add(callback);
|
|
108
|
+
return () => {
|
|
109
|
+
listeners.delete(callback);
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
setTools: (next) => {
|
|
113
|
+
tools = next;
|
|
114
|
+
listeners.forEach((callback) => callback());
|
|
115
|
+
},
|
|
116
|
+
} as FakeProvider;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
let latest: Unstable_WebMcpProviderResult;
|
|
120
|
+
|
|
121
|
+
export const providerResult = (): Unstable_WebMcpProviderResult => latest;
|
|
122
|
+
|
|
123
|
+
const Probe = ({ options }: { options: Unstable_WebMcpProviderOptions }) => {
|
|
124
|
+
latest = unstable_useWebMcpProvider(options);
|
|
125
|
+
return null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const Registrar = ({ provider }: { provider: ModelContextProvider }) => {
|
|
129
|
+
const aui = useAui();
|
|
130
|
+
useEffect(() => aui.modelContext.register(provider), [aui, provider]);
|
|
131
|
+
return null;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const Harness = ({
|
|
135
|
+
provider,
|
|
136
|
+
options,
|
|
137
|
+
}: {
|
|
138
|
+
provider: ModelContextProvider;
|
|
139
|
+
options: Unstable_WebMcpProviderOptions;
|
|
140
|
+
}) => (
|
|
141
|
+
<AuiProvider config={AuiConfig({ modelContext: ModelContext() } as never)}>
|
|
142
|
+
<Registrar provider={provider} />
|
|
143
|
+
<Probe options={options} />
|
|
144
|
+
</AuiProvider>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
export const mountProvider = (
|
|
148
|
+
provider: ModelContextProvider,
|
|
149
|
+
options: Unstable_WebMcpProviderOptions = {},
|
|
150
|
+
wrap: (children: ReactNode) => ReactNode = (children) => children,
|
|
151
|
+
) => {
|
|
152
|
+
const view = render(
|
|
153
|
+
<>{wrap(<Harness provider={provider} options={options} />)}</>,
|
|
154
|
+
);
|
|
155
|
+
return {
|
|
156
|
+
view,
|
|
157
|
+
rerender: (next: Unstable_WebMcpProviderOptions) =>
|
|
158
|
+
view.rerender(
|
|
159
|
+
<>{wrap(<Harness provider={provider} options={next} />)}</>,
|
|
160
|
+
),
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const waitForNames = (names: string[]) =>
|
|
165
|
+
vi.waitFor(() => expect(providerResult().registeredToolNames).toEqual(names));
|
|
166
|
+
|
|
167
|
+
export const silenceWarnings = () =>
|
|
168
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { ToolResponse, type Tool } from "assistant-stream";
|
|
4
|
+
import {
|
|
5
|
+
defaultWebMcpFilter,
|
|
6
|
+
toMcpContent,
|
|
7
|
+
toWebMcpTool,
|
|
8
|
+
} from "./convertTools";
|
|
9
|
+
|
|
10
|
+
const jsonSchema = {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: { city: { type: "string" } },
|
|
13
|
+
required: ["city"],
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
const frontendTool = (
|
|
17
|
+
overrides: Partial<Tool<any, any>> = {},
|
|
18
|
+
): Tool<any, any> =>
|
|
19
|
+
({
|
|
20
|
+
type: "frontend",
|
|
21
|
+
description: "Get the weather for a city.",
|
|
22
|
+
parameters: jsonSchema,
|
|
23
|
+
execute: async ({ city }: { city: string }) => `Sunny in ${city}`,
|
|
24
|
+
...overrides,
|
|
25
|
+
}) as Tool<any, any>;
|
|
26
|
+
|
|
27
|
+
const descriptorFor = (
|
|
28
|
+
overrides: Partial<Tool<any, any>> = {},
|
|
29
|
+
lifecycleSignal?: AbortSignal,
|
|
30
|
+
) => toWebMcpTool("t", () => frontendTool(overrides), lifecycleSignal);
|
|
31
|
+
|
|
32
|
+
const text = (value: string) => ({ type: "text", text: value });
|
|
33
|
+
|
|
34
|
+
describe("defaultWebMcpFilter", () => {
|
|
35
|
+
it.for([
|
|
36
|
+
["exposes an enabled frontend tool", {}, true],
|
|
37
|
+
["hides a backend tool", { type: "backend" }, false],
|
|
38
|
+
["hides a frontend tool with no execute", { execute: undefined }, false],
|
|
39
|
+
["hides a disabled frontend tool", { disabled: true }, false],
|
|
40
|
+
["exposes a tool authored without a type", { type: undefined }, true],
|
|
41
|
+
[
|
|
42
|
+
"hides a type-less tool with no execute",
|
|
43
|
+
{ type: undefined, execute: undefined },
|
|
44
|
+
false,
|
|
45
|
+
],
|
|
46
|
+
] as const)("%s", ([, overrides, expected]) => {
|
|
47
|
+
expect(defaultWebMcpFilter("t", frontendTool(overrides as any))).toBe(
|
|
48
|
+
expected,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("toWebMcpTool descriptor", () => {
|
|
54
|
+
it("projects name, description, and the input schema", () => {
|
|
55
|
+
const descriptor = toWebMcpTool("get_weather", () => frontendTool());
|
|
56
|
+
expect(descriptor.name).toBe("get_weather");
|
|
57
|
+
expect(descriptor.description).toBe("Get the weather for a city.");
|
|
58
|
+
expect(descriptor.inputSchema).toEqual(jsonSchema);
|
|
59
|
+
|
|
60
|
+
expect(
|
|
61
|
+
descriptorFor({ parameters: z.object({ city: z.string() }) }).inputSchema,
|
|
62
|
+
).toMatchObject(jsonSchema);
|
|
63
|
+
|
|
64
|
+
const bare = descriptorFor({
|
|
65
|
+
description: undefined,
|
|
66
|
+
parameters: undefined,
|
|
67
|
+
});
|
|
68
|
+
expect(bare.description).toBe("");
|
|
69
|
+
expect(bare.inputSchema).toEqual({ type: "object", properties: {} });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("throws at construction for a schema that cannot convert", () => {
|
|
73
|
+
const badSchema = {
|
|
74
|
+
"~standard": { version: 1, validate: () => ({ issues: undefined }) },
|
|
75
|
+
};
|
|
76
|
+
expect(() => descriptorFor({ parameters: badSchema as any })).toThrow();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("toWebMcpTool execute", () => {
|
|
81
|
+
it("passes the arguments through, defaulting missing arguments to {}", async () => {
|
|
82
|
+
const execute = vi.fn(async () => "Sunny in Paris");
|
|
83
|
+
const descriptor = descriptorFor({ execute, parameters: undefined });
|
|
84
|
+
|
|
85
|
+
const result = await descriptor.execute({ city: "Paris" });
|
|
86
|
+
expect(execute).toHaveBeenCalledWith(
|
|
87
|
+
{ city: "Paris" },
|
|
88
|
+
expect.objectContaining({ toolCallId: expect.any(String) }),
|
|
89
|
+
);
|
|
90
|
+
expect(result).toEqual({ content: [text("Sunny in Paris")] });
|
|
91
|
+
|
|
92
|
+
await descriptor.execute(undefined);
|
|
93
|
+
expect(execute).toHaveBeenLastCalledWith({}, expect.anything());
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it.for([
|
|
97
|
+
["serializes a non-string result", { ok: true }, '{"ok":true}'],
|
|
98
|
+
[
|
|
99
|
+
"falls back to String() for an unserializable result",
|
|
100
|
+
Symbol("opaque"),
|
|
101
|
+
"Symbol(opaque)",
|
|
102
|
+
],
|
|
103
|
+
] as const)("%s", async ([, value, expected]) => {
|
|
104
|
+
const result = await descriptorFor({
|
|
105
|
+
execute: async () => value,
|
|
106
|
+
}).execute({});
|
|
107
|
+
expect(result).toEqual({ content: [text(expected)] });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("reports a thrown value as an error result", async () => {
|
|
111
|
+
const throwing = (value: unknown) =>
|
|
112
|
+
descriptorFor({
|
|
113
|
+
execute: async () => {
|
|
114
|
+
throw value;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
await expect(throwing(new Error("boom")).execute({})).resolves.toEqual({
|
|
118
|
+
isError: true,
|
|
119
|
+
content: [text("boom")],
|
|
120
|
+
});
|
|
121
|
+
await expect(throwing("raw string boom").execute({})).resolves.toEqual({
|
|
122
|
+
isError: true,
|
|
123
|
+
content: [text("raw string boom")],
|
|
124
|
+
});
|
|
125
|
+
await expect(throwing({ code: 500 }).execute({})).resolves.toEqual({
|
|
126
|
+
isError: true,
|
|
127
|
+
content: [text("[object Object]")],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const circular: Record<string, unknown> = { a: 1 };
|
|
131
|
+
circular["self"] = circular;
|
|
132
|
+
const unserializable = await descriptorFor({
|
|
133
|
+
execute: async () => circular,
|
|
134
|
+
}).execute({});
|
|
135
|
+
expect(unserializable.isError).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("reports an error when a published tool has no client-side execute", async () => {
|
|
139
|
+
await expect(
|
|
140
|
+
descriptorFor({ execute: undefined }).execute({}),
|
|
141
|
+
).resolves.toEqual({
|
|
142
|
+
isError: true,
|
|
143
|
+
content: [text('Tool "t" has no client-side implementation.')],
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("projects values JSON cannot serialize but that have a string form", async () => {
|
|
148
|
+
await expect(
|
|
149
|
+
descriptorFor({ execute: async () => 9007199254740993n }).execute({}),
|
|
150
|
+
).resolves.toEqual({ content: [text("9007199254740993")] });
|
|
151
|
+
await expect(
|
|
152
|
+
descriptorFor({ execute: async () => Symbol("ticket") }).execute({}),
|
|
153
|
+
).resolves.toEqual({ content: [text("Symbol(ticket)")] });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("rejects human input requests", async () => {
|
|
157
|
+
const result = await descriptorFor({
|
|
158
|
+
execute: async (_args: unknown, context: any) => await context.human(),
|
|
159
|
+
}).execute({});
|
|
160
|
+
expect(result).toEqual({
|
|
161
|
+
isError: true,
|
|
162
|
+
content: [text("human input not supported in WebMCP context")],
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("toWebMcpTool schema validation", () => {
|
|
168
|
+
const zodTool = (overrides: Partial<Tool<any, any>> = {}) =>
|
|
169
|
+
descriptorFor({
|
|
170
|
+
parameters: z.object({ city: z.string() }),
|
|
171
|
+
...overrides,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("runs execute unchanged when the arguments validate", async () => {
|
|
175
|
+
const execute = vi.fn(async () => "ok");
|
|
176
|
+
const result = await zodTool({ execute }).execute({ city: "Paris" });
|
|
177
|
+
expect(execute).toHaveBeenCalledWith({ city: "Paris" }, expect.anything());
|
|
178
|
+
expect(result).toEqual({ content: [text("ok")] });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("returns a validation error when the arguments do not validate", async () => {
|
|
182
|
+
const execute = vi.fn(async () => "ok");
|
|
183
|
+
const result = await zodTool({ execute }).execute({ city: 42 });
|
|
184
|
+
expect(execute).not.toHaveBeenCalled();
|
|
185
|
+
expect(result.isError).toBe(true);
|
|
186
|
+
expect(result.content[0]).toMatchObject({
|
|
187
|
+
text: expect.stringContaining("Function parameter validation failed."),
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("hands invalid arguments to experimental_onSchemaValidationError", async () => {
|
|
192
|
+
const execute = vi.fn(async () => "ok");
|
|
193
|
+
const result = await zodTool({
|
|
194
|
+
execute,
|
|
195
|
+
experimental_onSchemaValidationError: async () => "recovered",
|
|
196
|
+
}).execute({ city: 42 });
|
|
197
|
+
expect(execute).not.toHaveBeenCalled();
|
|
198
|
+
expect(result).toEqual({ content: [text("recovered")] });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("awaits a validator that returns a non-Promise thenable", async () => {
|
|
202
|
+
const execute = vi.fn(async () => "ok");
|
|
203
|
+
const schema = z.object({ city: z.string() });
|
|
204
|
+
(schema as any)["~standard"] = {
|
|
205
|
+
...schema["~standard"],
|
|
206
|
+
validate: () => ({
|
|
207
|
+
then: (resolve: (value: { issues: unknown[] }) => void) => {
|
|
208
|
+
resolve({ issues: [{ message: "cross-realm" }] });
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const result = await descriptorFor({ execute, parameters: schema }).execute(
|
|
214
|
+
{ city: 42 },
|
|
215
|
+
);
|
|
216
|
+
expect(execute).not.toHaveBeenCalled();
|
|
217
|
+
expect(result.isError).toBe(true);
|
|
218
|
+
expect(result.content[0]).toMatchObject({
|
|
219
|
+
text: expect.stringContaining("cross-realm"),
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("awaits an async Standard Schema validation", async () => {
|
|
224
|
+
const execute = vi.fn(async () => "ok");
|
|
225
|
+
const schema = z.object({ city: z.string() });
|
|
226
|
+
const sync = schema["~standard"].validate;
|
|
227
|
+
(schema as any)["~standard"] = {
|
|
228
|
+
...schema["~standard"],
|
|
229
|
+
validate: async (value: unknown) => sync(value),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const result = await descriptorFor({ execute, parameters: schema }).execute(
|
|
233
|
+
{ city: 42 },
|
|
234
|
+
);
|
|
235
|
+
expect(execute).not.toHaveBeenCalled();
|
|
236
|
+
expect(result.isError).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("toWebMcpTool cancellation", () => {
|
|
241
|
+
it("refuses to run once its lifecycle signal is aborted", async () => {
|
|
242
|
+
const execute = vi.fn(async () => "never");
|
|
243
|
+
const controller = new AbortController();
|
|
244
|
+
controller.abort();
|
|
245
|
+
const result = await descriptorFor({ execute }, controller.signal).execute(
|
|
246
|
+
{},
|
|
247
|
+
);
|
|
248
|
+
expect(execute).not.toHaveBeenCalled();
|
|
249
|
+
expect(result).toEqual({
|
|
250
|
+
isError: true,
|
|
251
|
+
content: [text('Tool "t" is no longer registered')],
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("refuses to run when the caller signal is already aborted", async () => {
|
|
256
|
+
const execute = vi.fn(async () => "never");
|
|
257
|
+
const controller = new AbortController();
|
|
258
|
+
controller.abort();
|
|
259
|
+
const result = await descriptorFor({ execute }).execute(
|
|
260
|
+
{},
|
|
261
|
+
{ signal: controller.signal },
|
|
262
|
+
);
|
|
263
|
+
expect(execute).not.toHaveBeenCalled();
|
|
264
|
+
expect(result).toEqual({
|
|
265
|
+
isError: true,
|
|
266
|
+
content: [text("Tool execution was cancelled.")],
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("merges the caller signal with the lifecycle signal", async () => {
|
|
271
|
+
const lifecycle = new AbortController();
|
|
272
|
+
const caller = new AbortController();
|
|
273
|
+
const descriptor = descriptorFor(
|
|
274
|
+
{
|
|
275
|
+
execute: async (_args: unknown, context: any) =>
|
|
276
|
+
new Promise((_resolve, reject) => {
|
|
277
|
+
context.abortSignal.addEventListener("abort", () =>
|
|
278
|
+
reject(new Error("aborted")),
|
|
279
|
+
);
|
|
280
|
+
}),
|
|
281
|
+
},
|
|
282
|
+
lifecycle.signal,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const pending = descriptor.execute({}, { signal: caller.signal });
|
|
286
|
+
lifecycle.abort();
|
|
287
|
+
await expect(pending).resolves.toEqual({
|
|
288
|
+
isError: true,
|
|
289
|
+
content: [text("aborted")],
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("returns an error result when the caller signal cannot be merged", async () => {
|
|
294
|
+
const execute = vi.fn(async () => "never");
|
|
295
|
+
const foreignSignal = {
|
|
296
|
+
aborted: false,
|
|
297
|
+
addEventListener: () => {},
|
|
298
|
+
removeEventListener: () => {},
|
|
299
|
+
} as unknown as AbortSignal;
|
|
300
|
+
|
|
301
|
+
const result = await descriptorFor(
|
|
302
|
+
{ execute },
|
|
303
|
+
new AbortController().signal,
|
|
304
|
+
).execute({}, { signal: foreignSignal });
|
|
305
|
+
expect(result.isError).toBe(true);
|
|
306
|
+
expect(execute).not.toHaveBeenCalled();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe("toMcpContent", () => {
|
|
311
|
+
const options = { tool: frontendTool(), toolCallId: "1", args: {} };
|
|
312
|
+
|
|
313
|
+
it.for([
|
|
314
|
+
["text parts", [{ type: "text", text: "hello" }], [text("hello")]],
|
|
315
|
+
[
|
|
316
|
+
"image file parts",
|
|
317
|
+
[{ type: "file", data: "AAA", mediaType: "image/png" }],
|
|
318
|
+
[{ type: "image", data: "AAA", mimeType: "image/png" }],
|
|
319
|
+
],
|
|
320
|
+
[
|
|
321
|
+
"image file parts with no data",
|
|
322
|
+
[{ type: "file", mediaType: "image/png" }],
|
|
323
|
+
[{ type: "image", data: "", mimeType: "image/png" }],
|
|
324
|
+
],
|
|
325
|
+
[
|
|
326
|
+
"non-image file parts",
|
|
327
|
+
[{ type: "file", data: "raw", mediaType: "text/plain" }],
|
|
328
|
+
[text("raw")],
|
|
329
|
+
],
|
|
330
|
+
[
|
|
331
|
+
"unknown parts",
|
|
332
|
+
[{ type: "reasoning", text: "why" }],
|
|
333
|
+
[text('{"type":"reasoning","text":"why"}')],
|
|
334
|
+
],
|
|
335
|
+
] as const)("maps %s", async ([, modelContent, expected]) => {
|
|
336
|
+
const response = await toMcpContent(
|
|
337
|
+
new ToolResponse({ result: "r", modelContent: modelContent as any }),
|
|
338
|
+
options,
|
|
339
|
+
);
|
|
340
|
+
expect(response).toEqual({ content: expected });
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("marks an error response, with or without an explicit modelContent", async () => {
|
|
344
|
+
await expect(
|
|
345
|
+
toMcpContent(
|
|
346
|
+
new ToolResponse({
|
|
347
|
+
result: "r",
|
|
348
|
+
isError: true,
|
|
349
|
+
modelContent: [{ type: "text", text: "failed" }],
|
|
350
|
+
}),
|
|
351
|
+
options,
|
|
352
|
+
),
|
|
353
|
+
).resolves.toEqual({ isError: true, content: [text("failed")] });
|
|
354
|
+
|
|
355
|
+
await expect(
|
|
356
|
+
toMcpContent(
|
|
357
|
+
new ToolResponse({ result: "went wrong", isError: true }),
|
|
358
|
+
options,
|
|
359
|
+
),
|
|
360
|
+
).resolves.toEqual({ isError: true, content: [text("went wrong")] });
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("projects a successful result through toModelOutput", async () => {
|
|
364
|
+
const toModelOutput = vi.fn(async () => [
|
|
365
|
+
{ type: "text", text: "projected" },
|
|
366
|
+
]);
|
|
367
|
+
const response = await toMcpContent("raw", {
|
|
368
|
+
...options,
|
|
369
|
+
tool: frontendTool({ toModelOutput } as any),
|
|
370
|
+
});
|
|
371
|
+
expect(toModelOutput).toHaveBeenCalledWith({
|
|
372
|
+
toolCallId: "1",
|
|
373
|
+
input: {},
|
|
374
|
+
output: "raw",
|
|
375
|
+
});
|
|
376
|
+
expect(response).toEqual({ content: [text("projected")] });
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("falls back to the default projection when toModelOutput throws", async () => {
|
|
380
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
381
|
+
const response = await toMcpContent("raw", {
|
|
382
|
+
...options,
|
|
383
|
+
tool: frontendTool({
|
|
384
|
+
toModelOutput: () => {
|
|
385
|
+
throw new Error("bad projection");
|
|
386
|
+
},
|
|
387
|
+
} as any),
|
|
388
|
+
});
|
|
389
|
+
expect(response).toEqual({ content: [text("raw")] });
|
|
390
|
+
expect(warn).toHaveBeenCalled();
|
|
391
|
+
warn.mockRestore();
|
|
392
|
+
});
|
|
393
|
+
});
|