@assistant-ui/react 0.15.17 → 0.15.19
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 +8 -2
- package/dist/context/providers/ThreadViewportProvider.js +2 -2
- package/dist/context/providers/ThreadViewportProvider.js.map +1 -1
- package/dist/hooks/useToolCallElapsed.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +5 -3
- package/dist/mcp-apps/McpAppRenderer.d.ts +30 -8
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +91 -47
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/app-frame.d.ts.map +1 -1
- package/dist/mcp-apps/app-frame.js +57 -33
- package/dist/mcp-apps/app-frame.js.map +1 -1
- package/dist/mcp-apps/bridge.js +9 -4
- package/dist/mcp-apps/bridge.js.map +1 -1
- package/dist/mcp-apps/index.d.ts +2 -2
- package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.js +47 -79
- package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.js +61 -138
- package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +3 -8
- 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/trigger/TriggerPopoverAction.js +46 -15
- package/dist/primitives/composer/trigger/TriggerPopoverAction.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/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/triggerKeyboardResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js +53 -61
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.js +129 -141
- package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
- package/dist/primitives/message/MessageParts.js +2 -14
- package/dist/primitives/message/MessageParts.js.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts +3 -3
- package/dist/primitives/message/MessagePartsGrouped.js +2 -2
- package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.d.ts +2 -0
- package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.js +13 -36
- package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
- package/dist/primitives/reasoning/useScrollLock.js +3 -1
- package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +3 -3
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
- package/dist/sandbox-host/SandboxHost.js +36 -9
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.js +24 -2
- 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/useMessageStallDetection.d.ts +4 -5
- package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
- package/dist/unstable/useMessageStallDetection.js +16 -15
- package/dist/unstable/useMessageStallDetection.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 +2 -8
- 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 +161 -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 +106 -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 +10 -2
- package/dist/utils/Primitive.d.ts.map +1 -1
- package/dist/utils/Primitive.js +25 -6
- package/dist/utils/Primitive.js.map +1 -1
- package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
- package/dist/utils/getSelectionMessageId.js +17 -3
- package/dist/utils/getSelectionMessageId.js.map +1 -1
- package/package.json +13 -13
- package/src/context/providers/ThreadViewportProvider.tsx +2 -2
- package/src/hooks/useToolCallElapsed.ts +3 -0
- package/src/index.ts +14 -1
- package/src/mcp-apps/McpAppRenderer.test.tsx +253 -5
- package/src/mcp-apps/McpAppRenderer.tsx +147 -52
- package/src/mcp-apps/app-frame.test.tsx +437 -1
- package/src/mcp-apps/app-frame.tsx +76 -21
- package/src/mcp-apps/bridge.test.ts +98 -0
- package/src/mcp-apps/bridge.ts +14 -3
- package/src/mcp-apps/index.ts +5 -1
- package/src/primitives/assistantModal/AssistantModalRoot.test.tsx +123 -0
- package/src/primitives/assistantModal/AssistantModalRoot.tsx +11 -31
- package/src/primitives/assistantModal/AssistantModalTrigger.test.tsx +1 -16
- package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +43 -0
- package/src/primitives/composer/ComposerAttachmentDropzone.tsx +2 -10
- package/src/primitives/composer/ComposerInput.test.tsx +22 -0
- package/src/primitives/composer/ComposerInput.tsx +5 -9
- package/src/primitives/composer/ComposerInputPluginContext.tsx +0 -10
- package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +4 -2
- package/src/primitives/composer/trigger/TriggerPopoverBehavior.test.tsx +109 -0
- package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +4 -2
- package/src/primitives/composer/trigger/matchesTriggerItemQuery.test.ts +52 -0
- package/src/primitives/composer/trigger/matchesTriggerItemQuery.ts +13 -0
- package/src/primitives/composer/trigger/triggerKeyboardResource.ts +13 -7
- package/src/primitives/composer/trigger/triggerNavigationResource.ts +8 -13
- package/src/primitives/message/MessageParts.tsx +1 -21
- package/src/primitives/message/MessagePartsGrouped.test.tsx +93 -0
- package/src/primitives/message/MessagePartsGrouped.tsx +6 -6
- package/src/primitives/messagePart/MessagePartText.test.tsx +69 -0
- package/src/primitives/messagePart/MessagePartText.tsx +22 -9
- package/src/primitives/reasoning/useScrollLock.test.tsx +129 -0
- package/src/primitives/reasoning/useScrollLock.ts +15 -1
- package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +38 -1
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +7 -6
- package/src/sandbox-host/SandboxHost.test.tsx +160 -2
- package/src/sandbox-host/SandboxHost.tsx +50 -10
- package/src/tests/MessageParts.rendererOptions.test.tsx +117 -0
- package/src/tests/toolCallTiming.test.tsx +55 -1
- package/src/unstable/useComposerInputHistory.test.tsx +2 -1
- package/src/unstable/useLiveCompletionAdapter.test.tsx +202 -1
- package/src/unstable/useLiveCompletionAdapter.ts +38 -8
- package/src/unstable/useMentionAdapter.test.tsx +383 -0
- package/src/unstable/useMentionAdapter.ts +94 -57
- package/src/unstable/useMessageStallDetection.ts +26 -19
- package/src/unstable/useModelContextSnapshot.test.tsx +132 -0
- package/src/unstable/useModelContextSnapshot.ts +67 -0
- package/src/unstable/useSlashCommandAdapter.ts +2 -9
- 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 +553 -0
- package/src/unstable/webmcp/convertTools.ts +260 -0
- package/src/unstable/webmcp/useWebMcpProvider.test.tsx +466 -0
- package/src/unstable/webmcp/useWebMcpProvider.ts +148 -0
- package/src/unstable/webmcp/webmcp-host.test.ts +142 -0
- package/src/unstable/webmcp/webmcp-host.ts +83 -0
- package/src/utils/Primitive.test.tsx +46 -1
- package/src/utils/Primitive.tsx +37 -7
- package/src/utils/getSelectionMessageId.test.ts +176 -1
- package/src/utils/getSelectionMessageId.ts +21 -3
- 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 -19
- 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/utils/invokeCallbackSafely.ts +0 -11
- package/src/utils/useToolArgsFieldStatus.ts +0 -18
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSlashCommandAdapter.js","names":["useLayoutEffect","useMemo","useRef","Unstable_TriggerAdapter","Unstable_TriggerItem","Unstable_IconComponent","Unstable_SlashCommand","id","label","description","icon","execute","Unstable_UseSlashCommandAdapterOptions","commands","removeOnExecute","iconMap","Record","fallbackIcon","Unstable_SlashCommandAction","onExecute","item","unstable_useSlashCommandAdapter","options","adapter","action","commandsRef","committedItemsRef","undefined","nextItems","map","toItem","items","areTriggerItemsEqual","current","categories","categoryItems","search","query","lower","toLowerCase","filter","
|
|
1
|
+
{"version":3,"file":"useSlashCommandAdapter.js","names":["useLayoutEffect","useMemo","useRef","Unstable_TriggerAdapter","Unstable_TriggerItem","Unstable_IconComponent","matchesTriggerItemQuery","Unstable_SlashCommand","id","label","description","icon","execute","Unstable_UseSlashCommandAdapterOptions","commands","removeOnExecute","iconMap","Record","fallbackIcon","Unstable_SlashCommandAction","onExecute","item","unstable_useSlashCommandAdapter","options","adapter","action","commandsRef","committedItemsRef","undefined","nextItems","map","toItem","items","areTriggerItemsEqual","current","categories","categoryItems","search","query","lower","toLowerCase","filter","find","c","cmd","type","metadata","previous","next","length","every","index","nextItem"],"sources":["../../src/unstable/useSlashCommandAdapter.ts"],"sourcesContent":["\"use client\";\n\nimport { useLayoutEffect, useMemo, useRef } from \"react\";\nimport type {\n Unstable_TriggerAdapter,\n Unstable_TriggerItem,\n} from \"@assistant-ui/core\";\nimport type { Unstable_IconComponent } from \"./useMentionAdapter\";\nimport { matchesTriggerItemQuery } from \"../primitives/composer/trigger/matchesTriggerItemQuery\";\n\nexport type Unstable_SlashCommand = {\n readonly id: string;\n readonly label?: string | undefined;\n readonly description?: string | undefined;\n readonly icon?: string | undefined;\n readonly execute: () => void;\n};\n\nexport type Unstable_UseSlashCommandAdapterOptions = {\n readonly commands: readonly Unstable_SlashCommand[];\n /** Strip the trigger text from the composer after executing. @default false */\n readonly removeOnExecute?: boolean | undefined;\n /** Maps `metadata.icon` / `category.id` string keys to React components. */\n readonly iconMap?: Record<string, Unstable_IconComponent>;\n /** Fallback icon when no entry in `iconMap` matches. */\n readonly fallbackIcon?: Unstable_IconComponent;\n};\n\nexport type Unstable_SlashCommandAction = {\n readonly onExecute: (item: Unstable_TriggerItem) => void;\n readonly removeOnExecute?: boolean | undefined;\n};\n\n/**\n * @deprecated Under active development and may change without notice.\n *\n * Bundles slash command definitions (with inline `execute` callbacks) into\n * `{adapter, action}` that plug directly into `ComposerTriggerPopover`.\n * `execute` stays in the hook closure and is never attached to the returned\n * `TriggerItem`, keeping items serializable.\n *\n * @example\n * ```tsx\n * const slash = unstable_useSlashCommandAdapter({\n * commands: [\n * { id: \"summarize\", execute: () => runSummarize(), icon: \"FileText\" },\n * { id: \"translate\", execute: () => runTranslate(), icon: \"Languages\" },\n * ],\n * });\n *\n * <ComposerTriggerPopover char=\"/\" {...slash} />\n * ```\n */\nexport function unstable_useSlashCommandAdapter(\n options: Unstable_UseSlashCommandAdapterOptions,\n): {\n adapter: Unstable_TriggerAdapter;\n action: Unstable_SlashCommandAction;\n iconMap?: Record<string, Unstable_IconComponent>;\n fallbackIcon?: Unstable_IconComponent;\n} {\n const { commands, removeOnExecute } = options;\n\n const commandsRef = useRef(commands);\n const committedItemsRef = useRef<readonly Unstable_TriggerItem[]>(undefined);\n const nextItems = commands.map(toItem);\n // A referential cache, never state: `items` is always structurally equal to\n // `nextItems`, so the render-time read cannot change what the adapter shows.\n const items = areTriggerItemsEqual(committedItemsRef.current, nextItems)\n ? committedItemsRef.current\n : nextItems;\n\n // The adapter's callbacks must only observe commands from committed renders.\n useLayoutEffect(() => {\n commandsRef.current = commands;\n committedItemsRef.current = items;\n }, [commands, items]);\n\n const adapter = useMemo<Unstable_TriggerAdapter>(\n () => ({\n categories: () => [],\n categoryItems: () => [],\n search: (query: string) => {\n const lower = query.toLowerCase();\n return items.filter((item) => matchesTriggerItemQuery(item, lower));\n },\n }),\n [items],\n );\n\n const action = useMemo<Unstable_SlashCommandAction>(\n () => ({\n onExecute: (item) => {\n commandsRef.current.find((c) => c.id === item.id)?.execute();\n },\n ...(removeOnExecute !== undefined ? { removeOnExecute } : {}),\n }),\n [removeOnExecute],\n );\n\n return useMemo(\n () => ({\n adapter,\n action,\n ...(options.iconMap ? { iconMap: options.iconMap } : {}),\n ...(options.fallbackIcon ? { fallbackIcon: options.fallbackIcon } : {}),\n }),\n [adapter, action, options.iconMap, options.fallbackIcon],\n );\n}\n\nfunction toItem(cmd: Unstable_SlashCommand): Unstable_TriggerItem {\n return {\n id: cmd.id,\n type: \"command\",\n label: cmd.label ?? `/${cmd.id}`,\n ...(cmd.description !== undefined ? { description: cmd.description } : {}),\n ...(cmd.icon !== undefined ? { metadata: { icon: cmd.icon } } : {}),\n };\n}\n\nfunction areTriggerItemsEqual(\n previous: readonly Unstable_TriggerItem[] | undefined,\n next: readonly Unstable_TriggerItem[],\n): previous is readonly Unstable_TriggerItem[] {\n if (!previous || previous.length !== next.length) return false;\n\n return previous.every((item, index) => {\n const nextItem = next[index];\n return (\n nextItem !== undefined &&\n item.id === nextItem.id &&\n item.label === nextItem.label &&\n item.description === nextItem.description &&\n item.metadata?.icon === nextItem.metadata?.icon\n );\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAgBsB,gCACdC,SAMA;CACA,MAAM,EAAET,UAAUC,oBAAoBQ;CAEtC,MAAMG,cAAcxB,OAAOY,QAAQ;CACnC,MAAMa,oBAAoBzB,OAAwC0B,KAAAA,CAAS;CAC3E,MAAMC,YAAYf,SAASgB,IAAIC,MAAM;CAGrC,MAAMC,QAAQC,qBAAqBN,kBAAkBO,SAASL,SAAS,IACnEF,kBAAkBO,UAClBL;CAGJ7B,sBAAsB;EACpB0B,YAAYQ,UAAUpB;EACtBa,kBAAkBO,UAAUF;CAC9B,GAAG,CAAClB,UAAUkB,KAAK,CAAC;CAEpB,MAAMR,UAAUvB,eACP;EACLkC,kBAAkB,CAAA;EAClBC,qBAAqB,CAAA;EACrBC,SAASC,UAAkB;GACzB,MAAMC,QAAQD,MAAME,YAAY;GAChC,OAAOR,MAAMS,QAAQpB,SAASf,wBAAwBe,MAAMkB,KAAK,CAAC;EACpE;CACF,IACA,CAACP,KAAK,CACR;CAEA,MAAMP,SAASxB,eACN;EACLmB,YAAYC,SAAS;GACnBK,YAAYQ,QAAQQ,MAAMC,MAAMA,EAAEnC,OAAOa,KAAKb,EAAE,CAAC,EAAEI,QAAQ;EAC7D;EACA,GAAIG,oBAAoBa,KAAAA,IAAY,EAAEb,gBAAgB,IAAI,CAAC;CAC7D,IACA,CAACA,eAAe,CAClB;CAEA,OAAOd,eACE;EACLuB;EACAC;EACA,GAAIF,QAAQP,UAAU,EAAEA,SAASO,QAAQP,QAAQ,IAAI,CAAC;EACtD,GAAIO,QAAQL,eAAe,EAAEA,cAAcK,QAAQL,aAAa,IAAI,CAAC;CACvE,IACA;EAACM;EAASC;EAAQF,QAAQP;EAASO,QAAQL;CAAY,CACzD;AACF;AAEA,SAASa,OAAOa,KAAkD;CAChE,OAAO;EACLpC,IAAIoC,IAAIpC;EACRqC,MAAM;EACNpC,OAAOmC,IAAInC,SAAS,IAAImC,IAAIpC;EAC5B,GAAIoC,IAAIlC,gBAAgBkB,KAAAA,IAAY,EAAElB,aAAakC,IAAIlC,YAAY,IAAI,CAAC;EACxE,GAAIkC,IAAIjC,SAASiB,KAAAA,IAAY,EAAEkB,UAAU,EAAEnC,MAAMiC,IAAIjC,KAAK,EAAE,IAAI,CAAC;CACnE;AACF;AAEA,SAASsB,qBACPc,UACAC,MAC6C;CAC7C,IAAI,CAACD,YAAYA,SAASE,WAAWD,KAAKC,QAAQ,OAAO;CAEzD,OAAOF,SAASG,OAAO7B,MAAM8B,UAAU;EACrC,MAAMC,WAAWJ,KAAKG;EACtB,OACEC,aAAaxB,KAAAA,KACbP,KAAKb,OAAO4C,SAAS5C,MACrBa,KAAKZ,UAAU2C,SAAS3C,SACxBY,KAAKX,gBAAgB0C,SAAS1C,eAC9BW,KAAKyB,UAAUnC,SAASyC,SAASN,UAAUnC;CAE/C,CAAC;AACH"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { WebMcpHost } from "./webmcp-host.js";
|
|
2
|
+
import { Tool } from "assistant-stream";
|
|
3
|
+
//#region src/unstable/webmcp/WebMcpRegistrationResource.d.ts
|
|
4
|
+
type WebMcpRegistrationProps = {
|
|
5
|
+
host: WebMcpHost;
|
|
6
|
+
name: string;
|
|
7
|
+
/** Re-registers when it changes; an execute-only edit reads through instead. */
|
|
8
|
+
signature: string;
|
|
9
|
+
tool: Tool<any, any>;
|
|
10
|
+
};
|
|
11
|
+
declare const WebMcpRegistrationResource: import("@assistant-ui/tap").Resource<string | null, [WebMcpRegistrationProps]>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { WebMcpRegistrationProps, WebMcpRegistrationResource };
|
|
14
|
+
//# sourceMappingURL=WebMcpRegistrationResource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebMcpRegistrationResource.d.ts","names":[],"sources":["../../../src/unstable/webmcp/WebMcpRegistrationResource.ts"],"mappings":";;;KAMY;EACV,MAAM;EACN;;EAEA;EACA,MAAM;;cAgFK,wDAA0B,yBAAA"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { toWebMcpTool } from "./convertTools.js";
|
|
2
|
+
import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "@assistant-ui/tap/react-shim";
|
|
4
|
+
import { resource } from "@assistant-ui/tap";
|
|
5
|
+
//#region src/unstable/webmcp/WebMcpRegistrationResource.ts
|
|
6
|
+
const notPermitted = (error) => error?.name === "NotAllowedError";
|
|
7
|
+
const notPermittedMessage = (name) => `[assistant-ui] WebMCP registration for tool "${name}" was not permitted; the page's tools permission is disabled.`;
|
|
8
|
+
const useWebMcpRegistration = (t0) => {
|
|
9
|
+
const $ = c(12);
|
|
10
|
+
const { host, name, signature, tool } = t0;
|
|
11
|
+
const [refused, setRefused] = useState(false);
|
|
12
|
+
const toolRef = useRef(tool);
|
|
13
|
+
let t1;
|
|
14
|
+
let t2;
|
|
15
|
+
if ($[0] !== tool) {
|
|
16
|
+
t1 = () => {
|
|
17
|
+
toolRef.current = tool;
|
|
18
|
+
};
|
|
19
|
+
t2 = [tool];
|
|
20
|
+
$[0] = tool;
|
|
21
|
+
$[1] = t1;
|
|
22
|
+
$[2] = t2;
|
|
23
|
+
} else {
|
|
24
|
+
t1 = $[1];
|
|
25
|
+
t2 = $[2];
|
|
26
|
+
}
|
|
27
|
+
useEffect(t1, t2);
|
|
28
|
+
let t3;
|
|
29
|
+
if ($[3] !== host || $[4] !== name || $[5] !== refused) {
|
|
30
|
+
t3 = () => {
|
|
31
|
+
if (refused) return;
|
|
32
|
+
const lifecycle = new AbortController();
|
|
33
|
+
let live = true;
|
|
34
|
+
let dispose;
|
|
35
|
+
const refuse = (message, error) => {
|
|
36
|
+
if (!live) return;
|
|
37
|
+
live = false;
|
|
38
|
+
setRefused(true);
|
|
39
|
+
console.warn(message, error);
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
dispose = host.registerTool(toWebMcpTool(name, () => toolRef.current, lifecycle.signal), (error_1) => refuse(notPermitted(error_1) ? notPermittedMessage(name) : `[assistant-ui] WebMCP registration for tool "${name}" failed (name may already be registered).`, error_1));
|
|
43
|
+
} catch (t4) {
|
|
44
|
+
const error_0 = t4;
|
|
45
|
+
refuse(notPermitted(error_0) ? notPermittedMessage(name) : `[assistant-ui] Skipping WebMCP registration for tool "${name}": registerTool failed (name may already be registered).`, error_0);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
return () => {
|
|
49
|
+
live = false;
|
|
50
|
+
lifecycle.abort();
|
|
51
|
+
try {
|
|
52
|
+
dispose();
|
|
53
|
+
} catch (t5) {
|
|
54
|
+
const error_2 = t5;
|
|
55
|
+
console.warn(`[assistant-ui] Unregistering WebMCP tool "${name}" failed.`, error_2);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
$[3] = host;
|
|
60
|
+
$[4] = name;
|
|
61
|
+
$[5] = refused;
|
|
62
|
+
$[6] = t3;
|
|
63
|
+
} else t3 = $[6];
|
|
64
|
+
let t4;
|
|
65
|
+
if ($[7] !== host || $[8] !== name || $[9] !== refused || $[10] !== signature) {
|
|
66
|
+
t4 = [
|
|
67
|
+
host,
|
|
68
|
+
name,
|
|
69
|
+
signature,
|
|
70
|
+
refused
|
|
71
|
+
];
|
|
72
|
+
$[7] = host;
|
|
73
|
+
$[8] = name;
|
|
74
|
+
$[9] = refused;
|
|
75
|
+
$[10] = signature;
|
|
76
|
+
$[11] = t4;
|
|
77
|
+
} else t4 = $[11];
|
|
78
|
+
useEffect(t3, t4);
|
|
79
|
+
return refused ? null : name;
|
|
80
|
+
};
|
|
81
|
+
const WebMcpRegistrationResource = resource(useWebMcpRegistration);
|
|
82
|
+
//#endregion
|
|
83
|
+
export { WebMcpRegistrationResource };
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=WebMcpRegistrationResource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebMcpRegistrationResource.js","names":["useEffect","useRef","useState","resource","Tool","toWebMcpTool","WebMcpHost","WebMcpRegistrationProps","host","name","signature","tool","notPermitted","error","notPermittedMessage","useWebMcpRegistration","t0","$","_c","refused","setRefused","toolRef","t1","t2","current","t3","lifecycle","AbortController","live","dispose","refuse","message","console","warn","registerTool","signal","error_1","t4","abort","t5","WebMcpRegistrationResource"],"sources":["../../../src/unstable/webmcp/WebMcpRegistrationResource.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport { resource } from \"@assistant-ui/tap\";\nimport type { Tool } from \"assistant-stream\";\nimport { toWebMcpTool } from \"./convertTools\";\nimport type { WebMcpHost } from \"./webmcp-host\";\n\nexport type WebMcpRegistrationProps = {\n host: WebMcpHost;\n name: string;\n /** Re-registers when it changes; an execute-only edit reads through instead. */\n signature: string;\n tool: Tool<any, any>;\n};\n\n// The explainer specifies NotAllowedError for a page whose tools permission is\n// off, which a cross-origin iframe without allow=\"tools\" reaches routinely. It\n// documents no rejection for a name already taken, so that stays a hedge.\nconst notPermitted = (error: unknown) =>\n (error as { name?: unknown } | null | undefined)?.name === \"NotAllowedError\";\n\nconst notPermittedMessage = (name: string) =>\n `[assistant-ui] WebMCP registration for tool \"${name}\" was not permitted; the page's tools permission is disabled.`;\n\nconst useWebMcpRegistration = ({\n host,\n name,\n signature,\n tool,\n}: WebMcpRegistrationProps): string | null => {\n // A refused name is remembered for as long as the tool stays in the model\n // context, so a permanent collision warns once rather than on every sync.\n const [refused, setRefused] = useState(false);\n const toolRef = useRef(tool);\n // Commit phase, so an abandoned render cannot hand the host a tool the tree\n // never committed.\n useEffect(() => {\n toolRef.current = tool;\n }, [tool]);\n\n useEffect(() => {\n if (refused) return undefined;\n\n const lifecycle = new AbortController();\n let live = true;\n let dispose: (() => void) | undefined;\n\n const refuse = (message: string, error: unknown) => {\n if (!live) return;\n live = false;\n setRefused(true);\n console.warn(message, error);\n };\n\n try {\n dispose = host.registerTool(\n toWebMcpTool(name, () => toolRef.current, lifecycle.signal),\n (error) =>\n refuse(\n notPermitted(error)\n ? notPermittedMessage(name)\n : `[assistant-ui] WebMCP registration for tool \"${name}\" failed (name may already be registered).`,\n error,\n ),\n );\n } catch (error) {\n refuse(\n notPermitted(error)\n ? notPermittedMessage(name)\n : `[assistant-ui] Skipping WebMCP registration for tool \"${name}\": registerTool failed (name may already be registered).`,\n error,\n );\n return undefined;\n }\n\n return () => {\n live = false;\n lifecycle.abort();\n try {\n dispose();\n } catch (error) {\n console.warn(\n `[assistant-ui] Unregistering WebMCP tool \"${name}\" failed.`,\n error,\n );\n }\n };\n }, [host, name, signature, refused]);\n\n return refused ? null : name;\n};\n\nexport const WebMcpRegistrationResource = resource(useWebMcpRegistration);\n"],"mappings":";;;;;AAiBA,MAAMY,gBAAgBC,UACnBA,OAAiDJ,SAAS;AAE7D,MAAMK,uBAAuBL,SAC3B,gDAAgDA,KAAI;AAEtD,MAAMM,yBAAwBC,OAAA;CAAA,MAAAC,IAAAC,EAAA,EAAA;CAAC,MAAA,EAAAV,MAAAC,MAAAC,WAAAC,SAAAK;CAQ7B,MAAA,CAAAG,SAAAC,cAA8BlB,SAAS,KAAK;CAC5C,MAAAmB,UAAgBpB,OAAOU,IAAI;CAAE,IAAAW;CAAA,IAAAC;CAAA,IAAAN,EAAA,OAAAN,MAAA;EAGnBW,WAAA;GACRD,QAAOG,UAAWb;EAAH;EACdY,KAAA,CAACZ,IAAI;EAACM,EAAA,KAAAN;EAAAM,EAAA,KAAAK;EAAAL,EAAA,KAAAM;CAAA,OAAA;EAAAD,KAAAL,EAAA;EAAAM,KAAAN,EAAA;CAAA;CAFTjB,UAAUsB,IAEPC,EAAM;CAAC,IAAAE;CAAA,IAAAR,EAAA,OAAAT,QAAAS,EAAA,OAAAR,QAAAQ,EAAA,OAAAE,SAAA;EAEAM,WAAA;GACR,IAAIN,SAAO;GAEX,MAAAO,YAAkB,IAAIC,gBAAgB;GACtC,IAAAC,OAAW;GACPC,IAAAA;GAEJ,MAAAC,UAAeC,SAAAlB,UAAA;IACb,IAAI,CAACe,MAAI;IACTA,OAAOA;IACPR,WAAW,IAAI;IACfY,QAAOC,KAAMF,SAASlB,KAAK;GAAC;GAG9B,IAAA;IACEgB,UAAUrB,KAAI0B,aACZ7B,aAAaI,YAAYY,QAAOG,SAAUE,UAASS,MAAO,IAC1DC,YACEN,OACElB,aAAaC,OAEsF,IAD/FC,oBAAoBL,IAC2E,IAFnG,gDAEoDA,KAAI,6CACxDI,OACF,CACJ;GATO,SAAAwB,IAAA;IAUAxB,MAAAA,UAAAA;IACPiB,OACElB,aAAaC,OAE6G,IADtHC,oBAAoBL,IACkG,IAF1H,yDAE6DA,KAAI,2DACjEI,OACF;IAAC;GAAA;GAEF,aAEM;IACLe,OAAOA;IACPF,UAASY,MAAO;IAChB,IAAA;KACET,QAAQ;IAAC,SAAAU,IAAA;KACF1B,MAAAA,UAAAA;KACPmB,QAAOC,KACL,6CAA6CxB,KAAI,YACjDI,OACF;IAAC;GACF;EACF;EACFI,EAAA,KAAAT;EAAAS,EAAA,KAAAR;EAAAQ,EAAA,KAAAE;EAAAF,EAAA,KAAAQ;CAAA,OAAAA,KAAAR,EAAA;CAAA,IAAAoB;CAAA,IAAApB,EAAA,OAAAT,QAAAS,EAAA,OAAAR,QAAAQ,EAAA,OAAAE,WAAAF,EAAA,QAAAP,WAAA;EAAE2B,KAAA;GAAC7B;GAAMC;GAAMC;GAAWS;EAAO;EAACF,EAAA,KAAAT;EAAAS,EAAA,KAAAR;EAAAQ,EAAA,KAAAE;EAAAF,EAAA,MAAAP;EAAAO,EAAA,MAAAoB;CAAA,OAAAA,KAAApB,EAAA;CA/CnCjB,UAAUyB,IA+CPY,EAAgC;CAAC,OAE7BlB,UAAA,OAAAV;AAAqB;AAG9B,MAAa+B,6BAA6BrC,SAASY,qBAAqB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { WebMcpCallToolResult, WebMcpToolDescriptor } from "./webmcp-host.js";
|
|
2
|
+
import { Tool } from "assistant-stream";
|
|
3
|
+
//#region src/unstable/webmcp/convertTools.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The predicate the WebMCP provider uses when no `filter` is passed: an enabled
|
|
6
|
+
* frontend tool with a client-side `execute`. A tool authored without a `type`
|
|
7
|
+
* is included, because `execute` is what distinguishes the deprecated
|
|
8
|
+
* type-less form from a backend or human tool. A `filter` replaces this, so
|
|
9
|
+
* pass it through to narrow the default set rather than widen it.
|
|
10
|
+
*/
|
|
11
|
+
declare const defaultWebMcpFilter: (_name: string, tool: Tool<any, any>) => boolean;
|
|
12
|
+
declare const toWebMcpInputSchema: (tool: Tool<any, any>) => unknown;
|
|
13
|
+
declare const toMcpContent: (result: unknown, options: {
|
|
14
|
+
tool: Tool<any, any>;
|
|
15
|
+
toolCallId: string;
|
|
16
|
+
args: Record<string, unknown>;
|
|
17
|
+
}) => Promise<WebMcpCallToolResult>;
|
|
18
|
+
declare const toWebMcpTool: (name: string, getTool: () => Tool<any, any>, lifecycleSignal?: AbortSignal) => WebMcpToolDescriptor;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { defaultWebMcpFilter, toMcpContent, toWebMcpInputSchema, toWebMcpTool };
|
|
21
|
+
//# sourceMappingURL=convertTools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convertTools.d.ts","names":[],"sources":["../../../src/unstable/webmcp/convertTools.ts"],"mappings":";;;;;;;;;;cAoBa,sBACX,eACA,MAAM;cAMK,sBAAuB,MAAM;cA2C7B,eACX,iBACA;EACE,MAAM;EACN;EACA,MAAM;MAEP,QAAQ;cAwGE,eACX,cACA,eAAe,gBACf,kBAAkB,gBACjB"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { generateId } from "@assistant-ui/core/internal";
|
|
2
|
+
import { ToolResponse, toJSONSchema } from "assistant-stream";
|
|
3
|
+
//#region src/unstable/webmcp/convertTools.ts
|
|
4
|
+
/**
|
|
5
|
+
* The predicate the WebMCP provider uses when no `filter` is passed: an enabled
|
|
6
|
+
* frontend tool with a client-side `execute`. A tool authored without a `type`
|
|
7
|
+
* is included, because `execute` is what distinguishes the deprecated
|
|
8
|
+
* type-less form from a backend or human tool. A `filter` replaces this, so
|
|
9
|
+
* pass it through to narrow the default set rather than widen it.
|
|
10
|
+
*/
|
|
11
|
+
const defaultWebMcpFilter = (_name, tool) => (tool.type === "frontend" || tool.type === void 0) && !!tool.execute && !tool.disabled;
|
|
12
|
+
const toWebMcpInputSchema = (tool) => tool.parameters ? toJSONSchema(tool.parameters) : {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {}
|
|
15
|
+
};
|
|
16
|
+
const textContent = (text) => ({
|
|
17
|
+
type: "text",
|
|
18
|
+
text
|
|
19
|
+
});
|
|
20
|
+
const errorResult = (message) => ({
|
|
21
|
+
isError: true,
|
|
22
|
+
content: [textContent(message)]
|
|
23
|
+
});
|
|
24
|
+
const toText = (value) => {
|
|
25
|
+
if (typeof value === "string") return value;
|
|
26
|
+
if (typeof value === "bigint" || typeof value === "symbol") return value.toString();
|
|
27
|
+
return JSON.stringify(value) ?? String(value);
|
|
28
|
+
};
|
|
29
|
+
const primitiveContent = (value) => [textContent(toText(value))];
|
|
30
|
+
const mapModelContentPart = (part) => {
|
|
31
|
+
if (part.type === "text") return textContent(part.text ?? "");
|
|
32
|
+
if (part.type === "file") {
|
|
33
|
+
if (typeof part.mediaType === "string" && part.mediaType.startsWith("image/")) return {
|
|
34
|
+
type: "image",
|
|
35
|
+
data: part.data ?? "",
|
|
36
|
+
mimeType: part.mediaType
|
|
37
|
+
};
|
|
38
|
+
return textContent(part.data ?? "");
|
|
39
|
+
}
|
|
40
|
+
return textContent(toText(part));
|
|
41
|
+
};
|
|
42
|
+
const toMcpContent = async (result, options) => {
|
|
43
|
+
const response = ToolResponse.toResponse(result);
|
|
44
|
+
if (response.modelContent) {
|
|
45
|
+
const content = response.modelContent.map(mapModelContentPart);
|
|
46
|
+
return response.isError ? {
|
|
47
|
+
isError: true,
|
|
48
|
+
content
|
|
49
|
+
} : { content };
|
|
50
|
+
}
|
|
51
|
+
if (!response.isError && options.tool.toModelOutput) try {
|
|
52
|
+
return { content: (await options.tool.toModelOutput({
|
|
53
|
+
toolCallId: options.toolCallId,
|
|
54
|
+
input: options.args,
|
|
55
|
+
output: response.result
|
|
56
|
+
})).map(mapModelContentPart) };
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.warn("[assistant-ui] toModelOutput threw; falling back to default projection.", e);
|
|
59
|
+
}
|
|
60
|
+
const content = primitiveContent(response.result);
|
|
61
|
+
return response.isError ? {
|
|
62
|
+
isError: true,
|
|
63
|
+
content
|
|
64
|
+
} : { content };
|
|
65
|
+
};
|
|
66
|
+
const isStandardSchema = (schema) => typeof schema === "object" && schema !== null && "~standard" in schema && schema["~standard"].version === 1;
|
|
67
|
+
const TOOL_ABORTED = Symbol("assistant-ui.webmcp-tool-aborted");
|
|
68
|
+
const isThenable = (value) => typeof value?.then === "function";
|
|
69
|
+
const raceWithAbort = async (value, abortSignal) => {
|
|
70
|
+
let onAbort;
|
|
71
|
+
const abortPromise = new Promise((resolve) => {
|
|
72
|
+
onAbort = () => resolve(TOOL_ABORTED);
|
|
73
|
+
if (abortSignal.aborted) onAbort();
|
|
74
|
+
else abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
return await Promise.race([abortPromise, value]);
|
|
78
|
+
} finally {
|
|
79
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const combineAbortSignals = (callerSignal, lifecycleSignal) => {
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const teardown = [];
|
|
85
|
+
const cleanup = () => {
|
|
86
|
+
while (teardown.length) teardown.pop()();
|
|
87
|
+
};
|
|
88
|
+
const abort = (reason) => {
|
|
89
|
+
cleanup();
|
|
90
|
+
controller.abort(reason);
|
|
91
|
+
};
|
|
92
|
+
const listen = (signal) => {
|
|
93
|
+
const onAbort = () => abort(signal.reason);
|
|
94
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
95
|
+
teardown.push(() => signal.removeEventListener("abort", onAbort));
|
|
96
|
+
};
|
|
97
|
+
if (callerSignal.aborted) abort(callerSignal.reason);
|
|
98
|
+
else if (lifecycleSignal.aborted) abort(lifecycleSignal.reason);
|
|
99
|
+
else {
|
|
100
|
+
listen(callerSignal);
|
|
101
|
+
listen(lifecycleSignal);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
signal: controller.signal,
|
|
105
|
+
cleanup
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
const toWebMcpTool = (name, getTool, lifecycleSignal) => ({
|
|
109
|
+
name,
|
|
110
|
+
description: getTool().description ?? "",
|
|
111
|
+
inputSchema: toWebMcpInputSchema(getTool()),
|
|
112
|
+
execute: async (rawArgs, context) => {
|
|
113
|
+
if (lifecycleSignal?.aborted) return errorResult(`Tool "${name}" is no longer registered`);
|
|
114
|
+
const tool = getTool();
|
|
115
|
+
const args = rawArgs ?? {};
|
|
116
|
+
const toolCallId = generateId();
|
|
117
|
+
let cleanup;
|
|
118
|
+
try {
|
|
119
|
+
const callerSignal = context?.signal;
|
|
120
|
+
let abortSignal;
|
|
121
|
+
if (callerSignal && lifecycleSignal) {
|
|
122
|
+
const combined = combineAbortSignals(callerSignal, lifecycleSignal);
|
|
123
|
+
abortSignal = combined.signal;
|
|
124
|
+
cleanup = combined.cleanup;
|
|
125
|
+
} else abortSignal = callerSignal ?? lifecycleSignal;
|
|
126
|
+
if (abortSignal?.aborted) return errorResult("Tool execution was cancelled.");
|
|
127
|
+
let executeFn = tool.execute;
|
|
128
|
+
if (isStandardSchema(tool.parameters)) {
|
|
129
|
+
const result = tool.parameters["~standard"].validate(args);
|
|
130
|
+
const validation = isThenable(result) ? abortSignal ? await raceWithAbort(result, abortSignal) : await result : result;
|
|
131
|
+
if (validation === TOOL_ABORTED) return errorResult("Tool execution was cancelled.");
|
|
132
|
+
if (validation.issues) {
|
|
133
|
+
const issues = validation.issues;
|
|
134
|
+
executeFn = tool.experimental_onSchemaValidationError ?? (() => {
|
|
135
|
+
throw new Error(`Function parameter validation failed. ${JSON.stringify(issues)}`);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (abortSignal?.aborted) return errorResult("Tool execution was cancelled.");
|
|
140
|
+
if (!executeFn) return errorResult(`Tool "${name}" has no client-side implementation.`);
|
|
141
|
+
const result = await executeFn(args, {
|
|
142
|
+
toolCallId,
|
|
143
|
+
abortSignal: abortSignal ?? new AbortController().signal,
|
|
144
|
+
human: () => Promise.reject(/* @__PURE__ */ new Error("human input not supported in WebMCP context"))
|
|
145
|
+
});
|
|
146
|
+
return await toMcpContent(result, {
|
|
147
|
+
tool,
|
|
148
|
+
toolCallId,
|
|
149
|
+
args
|
|
150
|
+
});
|
|
151
|
+
} catch (e) {
|
|
152
|
+
return errorResult(e instanceof Error ? e.message : String(e));
|
|
153
|
+
} finally {
|
|
154
|
+
cleanup?.();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
//#endregion
|
|
159
|
+
export { defaultWebMcpFilter, toMcpContent, toWebMcpInputSchema, toWebMcpTool };
|
|
160
|
+
|
|
161
|
+
//# sourceMappingURL=convertTools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convertTools.js","names":["ToolResponse","toJSONSchema","Tool","ToolModelContentPart","generateId","WebMcpCallToolResult","WebMcpContent","WebMcpToolDescriptor","defaultWebMcpFilter","_name","tool","type","undefined","execute","disabled","toWebMcpInputSchema","parameters","properties","textContent","text","errorResult","message","isError","content","toText","value","toString","JSON","stringify","String","primitiveContent","mapModelContentPart","part","mediaType","startsWith","data","mimeType","toMcpContent","result","options","toolCallId","args","Record","Promise","response","toResponse","modelContent","map","toModelOutput","parts","input","output","e","console","warn","StandardSchemaLike","version","validate","issues","isStandardSchema","schema","TOOL_ABORTED","Symbol","isThenable","T","PromiseLike","then","raceWithAbort","abortSignal","AbortSignal","onAbort","abortPromise","resolve","aborted","addEventListener","once","race","removeEventListener","combineAbortSignals","callerSignal","lifecycleSignal","signal","cleanup","controller","AbortController","teardown","length","pop","abort","reason","listen","push","toWebMcpTool","name","getTool","description","inputSchema","rawArgs","context","combined","executeFn","validation","experimental_onSchemaValidationError","Error","human","reject"],"sources":["../../../src/unstable/webmcp/convertTools.ts"],"sourcesContent":["import {\n ToolResponse,\n toJSONSchema,\n type Tool,\n type ToolModelContentPart,\n} from \"assistant-stream\";\nimport { generateId } from \"@assistant-ui/core/internal\";\nimport type {\n WebMcpCallToolResult,\n WebMcpContent,\n WebMcpToolDescriptor,\n} from \"./webmcp-host\";\n\n/**\n * The predicate the WebMCP provider uses when no `filter` is passed: an enabled\n * frontend tool with a client-side `execute`. A tool authored without a `type`\n * is included, because `execute` is what distinguishes the deprecated\n * type-less form from a backend or human tool. A `filter` replaces this, so\n * pass it through to narrow the default set rather than widen it.\n */\nexport const defaultWebMcpFilter = (\n _name: string,\n tool: Tool<any, any>,\n): boolean =>\n (tool.type === \"frontend\" || tool.type === undefined) &&\n !!tool.execute &&\n !tool.disabled;\n\nexport const toWebMcpInputSchema = (tool: Tool<any, any>): unknown =>\n tool.parameters\n ? toJSONSchema(tool.parameters)\n : { type: \"object\", properties: {} };\n\nconst textContent = (text: string): WebMcpContent => ({ type: \"text\", text });\n\nconst errorResult = (message: string): WebMcpCallToolResult => ({\n isError: true,\n content: [textContent(message)],\n});\n\n// bigint throws in JSON.stringify and symbol serializes to undefined, but both\n// have a faithful string form. A value that cannot be serialized at all still\n// throws through to the error result.\nconst toText = (value: unknown): string => {\n if (typeof value === \"string\") return value;\n if (typeof value === \"bigint\" || typeof value === \"symbol\") {\n return value.toString();\n }\n return JSON.stringify(value) ?? String(value);\n};\n\nconst primitiveContent = (value: unknown): WebMcpContent[] => [\n textContent(toText(value)),\n];\n\nconst mapModelContentPart = (part: ToolModelContentPart): WebMcpContent => {\n if (part.type === \"text\") {\n return textContent(part.text ?? \"\");\n }\n if (part.type === \"file\") {\n if (\n typeof part.mediaType === \"string\" &&\n part.mediaType.startsWith(\"image/\")\n ) {\n return { type: \"image\", data: part.data ?? \"\", mimeType: part.mediaType };\n }\n return textContent(part.data ?? \"\");\n }\n return textContent(toText(part));\n};\n\nexport const toMcpContent = async (\n result: unknown,\n options: {\n tool: Tool<any, any>;\n toolCallId: string;\n args: Record<string, unknown>;\n },\n): Promise<WebMcpCallToolResult> => {\n const response = ToolResponse.toResponse(result);\n if (response.modelContent) {\n const content = response.modelContent.map(mapModelContentPart);\n return response.isError ? { isError: true, content } : { content };\n }\n if (!response.isError && options.tool.toModelOutput) {\n try {\n const parts = await options.tool.toModelOutput({\n toolCallId: options.toolCallId,\n input: options.args,\n output: response.result,\n });\n return { content: parts.map(mapModelContentPart) };\n } catch (e) {\n console.warn(\n \"[assistant-ui] toModelOutput threw; falling back to default projection.\",\n e,\n );\n }\n }\n const content = primitiveContent(response.result);\n return response.isError ? { isError: true, content } : { content };\n};\n\ntype StandardSchemaLike = {\n \"~standard\": {\n version: number;\n validate: (\n value: unknown,\n ) =>\n | { issues?: readonly unknown[] | undefined }\n | Promise<{ issues?: readonly unknown[] | undefined }>;\n };\n};\n\nconst isStandardSchema = (schema: unknown): schema is StandardSchemaLike =>\n typeof schema === \"object\" &&\n schema !== null &&\n \"~standard\" in schema &&\n (schema as StandardSchemaLike)[\"~standard\"].version === 1;\n\nconst TOOL_ABORTED = Symbol(\"assistant-ui.webmcp-tool-aborted\");\n\nconst isThenable = <T>(value: T | PromiseLike<T>): value is PromiseLike<T> =>\n typeof (value as PromiseLike<T> | null | undefined)?.then === \"function\";\n\nconst raceWithAbort = async <T>(\n value: PromiseLike<T>,\n abortSignal: AbortSignal,\n): Promise<T | typeof TOOL_ABORTED> => {\n let onAbort!: () => void;\n const abortPromise = new Promise<typeof TOOL_ABORTED>((resolve) => {\n onAbort = () => resolve(TOOL_ABORTED);\n if (abortSignal.aborted) {\n onAbort();\n } else {\n abortSignal.addEventListener(\"abort\", onAbort, { once: true });\n }\n });\n\n try {\n // Unlike assistant-stream's helper, cancellation wins when validation aborts and rejects synchronously.\n return await Promise.race([abortPromise, value]);\n } finally {\n abortSignal.removeEventListener(\"abort\", onAbort);\n }\n};\n\n// AbortSignal.any sits above the browserslist floor and rejects any input that\n// is not a native AbortSignal, which a navigator.modelContext polyfill's signal\n// is not. The merged signal tracks its inputs only until cleanup runs, where\n// the single-signal path hands the caller the lifecycle signal itself.\nconst combineAbortSignals = (\n callerSignal: AbortSignal,\n lifecycleSignal: AbortSignal,\n): { signal: AbortSignal; cleanup: () => void } => {\n const controller = new AbortController();\n const teardown: (() => void)[] = [];\n const cleanup = () => {\n while (teardown.length) teardown.pop()!();\n };\n const abort = (reason: unknown) => {\n cleanup();\n controller.abort(reason);\n };\n const listen = (signal: AbortSignal) => {\n const onAbort = () => abort(signal.reason);\n signal.addEventListener(\"abort\", onAbort, { once: true });\n teardown.push(() => signal.removeEventListener(\"abort\", onAbort));\n };\n\n if (callerSignal.aborted) {\n abort(callerSignal.reason);\n } else if (lifecycleSignal.aborted) {\n abort(lifecycleSignal.reason);\n } else {\n listen(callerSignal);\n listen(lifecycleSignal);\n }\n\n return { signal: controller.signal, cleanup };\n};\n\nexport const toWebMcpTool = (\n name: string,\n getTool: () => Tool<any, any>,\n lifecycleSignal?: AbortSignal,\n): WebMcpToolDescriptor => ({\n name,\n description: getTool().description ?? \"\",\n inputSchema: toWebMcpInputSchema(getTool()),\n execute: async (rawArgs, context) => {\n if (lifecycleSignal?.aborted) {\n return errorResult(`Tool \"${name}\" is no longer registered`);\n }\n const tool = getTool();\n const args = (rawArgs ?? {}) as Record<string, unknown>;\n const toolCallId = generateId();\n let cleanup: (() => void) | undefined;\n try {\n const callerSignal = context?.signal;\n let abortSignal: AbortSignal | undefined;\n if (callerSignal && lifecycleSignal) {\n const combined = combineAbortSignals(callerSignal, lifecycleSignal);\n abortSignal = combined.signal;\n cleanup = combined.cleanup;\n } else {\n abortSignal = callerSignal ?? lifecycleSignal;\n }\n\n if (abortSignal?.aborted) {\n return errorResult(\"Tool execution was cancelled.\");\n }\n\n let executeFn = tool.execute;\n if (isStandardSchema(tool.parameters)) {\n const result = tool.parameters[\"~standard\"].validate(args);\n const validation = isThenable(result)\n ? abortSignal\n ? await raceWithAbort(result, abortSignal)\n : await result\n : result;\n if (validation === TOOL_ABORTED) {\n return errorResult(\"Tool execution was cancelled.\");\n }\n if (validation.issues) {\n const issues = validation.issues;\n executeFn =\n tool.experimental_onSchemaValidationError ??\n (() => {\n throw new Error(\n `Function parameter validation failed. ${JSON.stringify(issues)}`,\n );\n });\n }\n }\n\n if (abortSignal?.aborted) {\n return errorResult(\"Tool execution was cancelled.\");\n }\n\n if (!executeFn) {\n return errorResult(`Tool \"${name}\" has no client-side implementation.`);\n }\n\n const result = await executeFn(args, {\n toolCallId,\n abortSignal: abortSignal ?? new AbortController().signal,\n human: () =>\n Promise.reject(\n new Error(\"human input not supported in WebMCP context\"),\n ),\n });\n return await toMcpContent(result, { tool, toolCallId, args });\n } catch (e) {\n return errorResult(e instanceof Error ? e.message : String(e));\n } finally {\n cleanup?.();\n }\n },\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Tool } from "assistant-stream";
|
|
2
|
+
//#region src/unstable/webmcp/useWebMcpProvider.d.ts
|
|
3
|
+
type Unstable_WebMcpProviderOptions = {
|
|
4
|
+
filter?: (name: string, tool: Tool<any, any>) => boolean;
|
|
5
|
+
};
|
|
6
|
+
type Unstable_WebMcpProviderResult = {
|
|
7
|
+
status: "unsupported" | "active";
|
|
8
|
+
registeredToolNames: readonly string[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Publishes the frontend tools in the model context to a WebMCP-capable
|
|
12
|
+
* browser, so the user's own browser agent can call them.
|
|
13
|
+
*
|
|
14
|
+
* Returns `status: "unsupported"` when the page exposes no
|
|
15
|
+
* `document.modelContext` (or `navigator.modelContext`), and the sorted names
|
|
16
|
+
* of the tools currently registered with the host.
|
|
17
|
+
*/
|
|
18
|
+
declare const unstable_useWebMcpProvider: (options?: Unstable_WebMcpProviderOptions) => Unstable_WebMcpProviderResult;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { Unstable_WebMcpProviderOptions, Unstable_WebMcpProviderResult, unstable_useWebMcpProvider };
|
|
21
|
+
//# sourceMappingURL=useWebMcpProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useWebMcpProvider.d.ts","names":[],"sources":["../../../src/unstable/webmcp/useWebMcpProvider.ts"],"mappings":";;KAeY;EACV,UAAU,cAAc,MAAM;;KAGpB;EACV;EACA;;;;;;;;;;cAqGW,6BACX,UAAS,mCACR"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useModelContextSnapshot } from "../useModelContextSnapshot.js";
|
|
3
|
+
import { getDefaultWebMcpHost } from "./webmcp-host.js";
|
|
4
|
+
import { defaultWebMcpFilter, toWebMcpInputSchema } from "./convertTools.js";
|
|
5
|
+
import { WebMcpRegistrationResource } from "./WebMcpRegistrationResource.js";
|
|
6
|
+
import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
|
|
7
|
+
import { useAui } from "@assistant-ui/store";
|
|
8
|
+
import { useEffect, useState, useSyncExternalStore } from "@assistant-ui/tap/react-shim";
|
|
9
|
+
import { useResources, useTapRoot, withKey } from "@assistant-ui/tap";
|
|
10
|
+
import { shallowEqual } from "@assistant-ui/store/internal";
|
|
11
|
+
//#region src/unstable/webmcp/useWebMcpProvider.ts
|
|
12
|
+
const EMPTY_NAMES = Object.freeze([]);
|
|
13
|
+
const EMPTY_TOOLS = Object.freeze({});
|
|
14
|
+
const warned = /* @__PURE__ */ new WeakSet();
|
|
15
|
+
const signatures = /* @__PURE__ */ new WeakMap();
|
|
16
|
+
const signatureOf = (tool) => {
|
|
17
|
+
const cached = signatures.get(tool);
|
|
18
|
+
if (cached && cached.description === tool.description) return cached.signature;
|
|
19
|
+
const signature = `${tool.description ?? ""}\u0000${JSON.stringify(toWebMcpInputSchema(tool))}`;
|
|
20
|
+
signatures.set(tool, {
|
|
21
|
+
description: tool.description,
|
|
22
|
+
signature
|
|
23
|
+
});
|
|
24
|
+
return signature;
|
|
25
|
+
};
|
|
26
|
+
const NO_SUBSCRIPTION = () => {};
|
|
27
|
+
const modelContextToolSource = {
|
|
28
|
+
empty: EMPTY_TOOLS,
|
|
29
|
+
read: (aui) => aui.modelContext.getModelContext().tools ?? EMPTY_TOOLS,
|
|
30
|
+
subscribe: (aui, onChange) => aui.modelContext.subscribe?.(onChange) ?? NO_SUBSCRIPTION
|
|
31
|
+
};
|
|
32
|
+
const useStableNames = (names) => {
|
|
33
|
+
const [cell] = useState(() => ({ names: EMPTY_NAMES }));
|
|
34
|
+
const next = names.filter((name) => name !== null).sort();
|
|
35
|
+
const previous = cell.names;
|
|
36
|
+
if (shallowEqual(previous, next)) return previous;
|
|
37
|
+
cell.names = next;
|
|
38
|
+
return next;
|
|
39
|
+
};
|
|
40
|
+
const useWebMcpRegistry = (t0) => {
|
|
41
|
+
const $ = c(4);
|
|
42
|
+
const { aui, host, filter } = t0;
|
|
43
|
+
const tools = useModelContextSnapshot(aui, host.available, modelContextToolSource);
|
|
44
|
+
let elements;
|
|
45
|
+
if ($[0] !== filter || $[1] !== host || $[2] !== tools) {
|
|
46
|
+
elements = [];
|
|
47
|
+
for (const [name, tool] of Object.entries(tools)) try {
|
|
48
|
+
if (!filter(name, tool)) continue;
|
|
49
|
+
const signature = signatureOf(tool);
|
|
50
|
+
elements.push(withKey(name, WebMcpRegistrationResource({
|
|
51
|
+
host,
|
|
52
|
+
name,
|
|
53
|
+
signature,
|
|
54
|
+
tool
|
|
55
|
+
}), [
|
|
56
|
+
host,
|
|
57
|
+
name,
|
|
58
|
+
signature,
|
|
59
|
+
tool
|
|
60
|
+
]));
|
|
61
|
+
} catch (t1) {
|
|
62
|
+
const error = t1;
|
|
63
|
+
if (warned.has(tool)) continue;
|
|
64
|
+
warned.add(tool);
|
|
65
|
+
console.warn(`[assistant-ui] Skipping WebMCP registration for tool "${name}": filter or schema conversion failed.`, error);
|
|
66
|
+
}
|
|
67
|
+
$[0] = filter;
|
|
68
|
+
$[1] = host;
|
|
69
|
+
$[2] = tools;
|
|
70
|
+
$[3] = elements;
|
|
71
|
+
} else elements = $[3];
|
|
72
|
+
return useStableNames(useResources(elements));
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Publishes the frontend tools in the model context to a WebMCP-capable
|
|
76
|
+
* browser, so the user's own browser agent can call them.
|
|
77
|
+
*
|
|
78
|
+
* Returns `status: "unsupported"` when the page exposes no
|
|
79
|
+
* `document.modelContext` (or `navigator.modelContext`), and the sorted names
|
|
80
|
+
* of the tools currently registered with the host.
|
|
81
|
+
*/
|
|
82
|
+
const unstable_useWebMcpProvider = (options = {}) => {
|
|
83
|
+
const aui = useAui();
|
|
84
|
+
const [host] = useState(getDefaultWebMcpHost);
|
|
85
|
+
const filter = options.filter ?? defaultWebMcpFilter;
|
|
86
|
+
const root = useTapRoot(function WebMcpProviderRoot() {
|
|
87
|
+
return useWebMcpRegistry({
|
|
88
|
+
aui,
|
|
89
|
+
host,
|
|
90
|
+
filter
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
const registeredToolNames = useSyncExternalStore(root.subscribe, root.getValue, () => EMPTY_NAMES);
|
|
94
|
+
const [published, setPublished] = useState(false);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (host.available) setPublished(true);
|
|
97
|
+
}, [host]);
|
|
98
|
+
return {
|
|
99
|
+
status: published ? "active" : "unsupported",
|
|
100
|
+
registeredToolNames
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
//#endregion
|
|
104
|
+
export { unstable_useWebMcpProvider };
|
|
105
|
+
|
|
106
|
+
//# sourceMappingURL=useWebMcpProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useWebMcpProvider.js","names":["c","_c","useEffect","useState","useSyncExternalStore","useResources","useTapRoot","withKey","useAui","AssistantClient","Tool","getDefaultWebMcpHost","WebMcpHost","defaultWebMcpFilter","toWebMcpInputSchema","WebMcpRegistrationResource","shallowEqual","useModelContextSnapshot","ModelContextSnapshotSource","Unstable_WebMcpProviderOptions","filter","name","tool","Unstable_WebMcpProviderResult","status","registeredToolNames","EMPTY_NAMES","Object","freeze","EMPTY_TOOLS","Record","warned","WeakSet","signatures","WeakMap","description","signature","signatureOf","cached","get","JSON","stringify","set","NO_SUBSCRIPTION","modelContextToolSource","empty","read","aui","modelContext","getModelContext","tools","subscribe","onChange","useStableNames","names","cell","next","sort","previous","useWebMcpRegistry","t0","$","host","available","elements","entries","push","t1","error","has","add","console","warn","unstable_useWebMcpProvider","options","root","WebMcpProviderRoot","getValue","published","setPublished"],"sources":["../../../src/unstable/webmcp/useWebMcpProvider.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState, useSyncExternalStore } from \"react\";\nimport { useResources, useTapRoot, withKey } from \"@assistant-ui/tap\";\nimport { useAui, type AssistantClient } from \"@assistant-ui/store\";\nimport type { Tool } from \"assistant-stream\";\nimport { getDefaultWebMcpHost, type WebMcpHost } from \"./webmcp-host\";\nimport { defaultWebMcpFilter, toWebMcpInputSchema } from \"./convertTools\";\nimport { WebMcpRegistrationResource } from \"./WebMcpRegistrationResource\";\nimport { shallowEqual } from \"@assistant-ui/store/internal\";\nimport {\n useModelContextSnapshot,\n type ModelContextSnapshotSource,\n} from \"../useModelContextSnapshot\";\n\nexport type Unstable_WebMcpProviderOptions = {\n filter?: (name: string, tool: Tool<any, any>) => boolean;\n};\n\nexport type Unstable_WebMcpProviderResult = {\n status: \"unsupported\" | \"active\";\n registeredToolNames: readonly string[];\n};\n\nconst EMPTY_NAMES: readonly string[] = Object.freeze([]);\nconst EMPTY_TOOLS: Record<string, Tool<any, any>> = Object.freeze({});\n\n// The description is re-read on every sync so mutating it in place is\n// observed; the schema is converted only when the tool object itself changes.\n// Keyed on the tool so a schema or filter that throws warns once instead of\n// on every model-context notify.\nconst warned = new WeakSet<Tool<any, any>>();\n\nconst signatures = new WeakMap<\n Tool<any, any>,\n { description: string | undefined; signature: string }\n>();\n\nconst signatureOf = (tool: Tool<any, any>) => {\n const cached = signatures.get(tool);\n if (cached && cached.description === tool.description)\n return cached.signature;\n const signature = `${tool.description ?? \"\"}\\u0000${JSON.stringify(\n toWebMcpInputSchema(tool),\n )}`;\n signatures.set(tool, { description: tool.description, signature });\n return signature;\n};\n\nconst NO_SUBSCRIPTION = () => {};\n\n// No `isEqual`: a caller republishing an unchanged tool set is how it asks for\n// a re-sync, which is what re-attempts a tool whose filter has stopped throwing\n// and what picks up a description edited in place on a stable tool object.\nconst modelContextToolSource: ModelContextSnapshotSource<\n Record<string, Tool<any, any>>\n> = {\n empty: EMPTY_TOOLS,\n read: (aui) => aui.modelContext.getModelContext().tools ?? EMPTY_TOOLS,\n subscribe: (aui, onChange) =>\n aui.modelContext.subscribe?.(onChange) ?? NO_SUBSCRIPTION,\n};\n\nconst useStableNames = (names: readonly (string | null)[]) => {\n const [cell] = useState(() => ({ names: EMPTY_NAMES }));\n const next = names.filter((name): name is string => name !== null).sort();\n const previous = cell.names;\n if (shallowEqual(previous, next)) {\n return previous;\n }\n cell.names = next;\n return next;\n};\n\nconst useWebMcpRegistry = ({\n aui,\n host,\n filter,\n}: {\n aui: AssistantClient;\n host: WebMcpHost;\n filter: (name: string, tool: Tool<any, any>) => boolean;\n}) => {\n const tools = useModelContextSnapshot(\n aui,\n host.available,\n modelContextToolSource,\n );\n\n const elements = [];\n for (const [name, tool] of Object.entries(tools)) {\n try {\n if (!filter(name, tool)) continue;\n const signature = signatureOf(tool);\n elements.push(\n withKey(\n name,\n WebMcpRegistrationResource({ host, name, signature, tool }),\n [host, name, signature, tool],\n ),\n );\n } catch (error) {\n if (warned.has(tool)) continue;\n warned.add(tool);\n console.warn(\n `[assistant-ui] Skipping WebMCP registration for tool \"${name}\": filter or schema conversion failed.`,\n error,\n );\n }\n }\n\n return useStableNames(useResources(elements));\n};\n\n/**\n * Publishes the frontend tools in the model context to a WebMCP-capable\n * browser, so the user's own browser agent can call them.\n *\n * Returns `status: \"unsupported\"` when the page exposes no\n * `document.modelContext` (or `navigator.modelContext`), and the sorted names\n * of the tools currently registered with the host.\n */\nexport const unstable_useWebMcpProvider = (\n options: Unstable_WebMcpProviderOptions = {},\n): Unstable_WebMcpProviderResult => {\n const aui = useAui();\n const [host] = useState(getDefaultWebMcpHost);\n const filter = options.filter ?? defaultWebMcpFilter;\n\n const root = useTapRoot(function WebMcpProviderRoot() {\n return useWebMcpRegistry({ aui, host, filter });\n });\n const registeredToolNames = useSyncExternalStore(\n root.subscribe,\n root.getValue,\n () => EMPTY_NAMES,\n );\n\n const [published, setPublished] = useState(false);\n useEffect(() => {\n if (host.available) setPublished(true);\n }, [host]);\n\n return {\n status: published ? \"active\" : \"unsupported\",\n registeredToolNames,\n };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/unstable/webmcp/webmcp-host.d.ts
|
|
2
|
+
type WebMcpContent = {
|
|
3
|
+
type: "text";
|
|
4
|
+
text: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: "image";
|
|
7
|
+
data: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
};
|
|
10
|
+
type WebMcpCallToolResult = {
|
|
11
|
+
content: WebMcpContent[];
|
|
12
|
+
isError?: boolean;
|
|
13
|
+
};
|
|
14
|
+
type WebMcpToolDescriptor = {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
inputSchema: unknown;
|
|
18
|
+
execute: (args: unknown, context?: {
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}) => Promise<WebMcpCallToolResult>;
|
|
21
|
+
};
|
|
22
|
+
type WebMcpModelContext = {
|
|
23
|
+
/**
|
|
24
|
+
* Aborting `options.signal` is what unregisters the tool. The explainer
|
|
25
|
+
* defines no other removal method, and a name-keyed one could not be used
|
|
26
|
+
* safely: a later registration may already hold the name.
|
|
27
|
+
*/
|
|
28
|
+
registerTool(tool: WebMcpToolDescriptor, options?: {
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}): Promise<void> | void;
|
|
31
|
+
};
|
|
32
|
+
type WebMcpHost = {
|
|
33
|
+
available: boolean;
|
|
34
|
+
registerTool(def: WebMcpToolDescriptor, onError?: (error: unknown) => void): () => void;
|
|
35
|
+
};
|
|
36
|
+
declare const getDefaultWebMcpHost: () => WebMcpHost;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { WebMcpCallToolResult, WebMcpContent, WebMcpHost, WebMcpModelContext, WebMcpToolDescriptor, getDefaultWebMcpHost };
|
|
39
|
+
//# sourceMappingURL=webmcp-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webmcp-host.d.ts","names":[],"sources":["../../../src/unstable/webmcp/webmcp-host.ts"],"mappings":";KAAY;EACN;EAAc;;EACd;EAAe;EAAc;;KAEvB;EACV,SAAS;EACT;;KAGU;EACV;EACA;EACA;EACA,UACE,eACA;IAAY,SAAS;QAClB,QAAQ;;KAGH;;;;;;EAMV,aACE,MAAM,sBACN;IAAY,SAAS;MACpB;;KAGO;EACV;EACA,aACE,KAAK,sBACL,WAAW;;cAsCF,4BAA2B"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/unstable/webmcp/webmcp-host.ts
|
|
2
|
+
const isThenable = (value) => typeof value?.then === "function";
|
|
3
|
+
const resolveModelContext = () => {
|
|
4
|
+
const context = globalThis.document?.modelContext ?? globalThis.navigator?.modelContext;
|
|
5
|
+
if (!context) return void 0;
|
|
6
|
+
if (typeof context.registerTool === "function") return context;
|
|
7
|
+
console.warn("[assistant-ui] Ignoring a modelContext with no callable registerTool; WebMCP reports unsupported.");
|
|
8
|
+
};
|
|
9
|
+
const createHost = (context) => ({
|
|
10
|
+
available: true,
|
|
11
|
+
registerTool: (def, onError) => {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const handle = context.registerTool(def, { signal: controller.signal });
|
|
14
|
+
if (isThenable(handle)) handle.then(() => {}, (error) => onError?.(error));
|
|
15
|
+
return () => controller.abort();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const getDefaultWebMcpHost = () => {
|
|
19
|
+
const context = resolveModelContext();
|
|
20
|
+
if (!context) return {
|
|
21
|
+
available: false,
|
|
22
|
+
registerTool: () => () => {}
|
|
23
|
+
};
|
|
24
|
+
return createHost(context);
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { getDefaultWebMcpHost };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=webmcp-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webmcp-host.js","names":["WebMcpContent","type","text","data","mimeType","WebMcpCallToolResult","content","isError","WebMcpToolDescriptor","name","description","inputSchema","execute","args","context","signal","AbortSignal","Promise","WebMcpModelContext","registerTool","tool","options","WebMcpHost","available","def","onError","error","ModelContextHost","modelContext","isThenable","value","then","resolveModelContext","globalThis","document","navigator","undefined","console","warn","createHost","controller","AbortController","handle","abort","getDefaultWebMcpHost"],"sources":["../../../src/unstable/webmcp/webmcp-host.ts"],"sourcesContent":["export type WebMcpContent =\n | { type: \"text\"; text: string }\n | { type: \"image\"; data: string; mimeType: string };\n\nexport type WebMcpCallToolResult = {\n content: WebMcpContent[];\n isError?: boolean;\n};\n\nexport type WebMcpToolDescriptor = {\n name: string;\n description: string;\n inputSchema: unknown;\n execute: (\n args: unknown,\n context?: { signal?: AbortSignal },\n ) => Promise<WebMcpCallToolResult>;\n};\n\nexport type WebMcpModelContext = {\n /**\n * Aborting `options.signal` is what unregisters the tool. The explainer\n * defines no other removal method, and a name-keyed one could not be used\n * safely: a later registration may already hold the name.\n */\n registerTool(\n tool: WebMcpToolDescriptor,\n options?: { signal?: AbortSignal },\n ): Promise<void> | void;\n};\n\nexport type WebMcpHost = {\n available: boolean;\n registerTool(\n def: WebMcpToolDescriptor,\n onError?: (error: unknown) => void,\n ): () => void;\n};\n\ntype ModelContextHost = { modelContext?: WebMcpModelContext } | undefined;\n\nconst isThenable = (value: unknown): value is Promise<unknown> =>\n typeof (value as { then?: unknown } | null | undefined)?.then === \"function\";\n\n// The explainer puts modelContext on document; navigator is tolerated for\n// older drafts and extensions that still inject it there.\nconst resolveModelContext = (): WebMcpModelContext | undefined => {\n const context =\n (globalThis.document as ModelContextHost)?.modelContext ??\n (globalThis.navigator as ModelContextHost)?.modelContext;\n if (!context) return undefined;\n if (typeof context.registerTool === \"function\") return context;\n console.warn(\n \"[assistant-ui] Ignoring a modelContext with no callable registerTool; WebMCP reports unsupported.\",\n );\n return undefined;\n};\n\nconst createHost = (context: WebMcpModelContext): WebMcpHost => ({\n available: true,\n registerTool: (def, onError) => {\n const controller = new AbortController();\n const handle = context.registerTool(def, { signal: controller.signal });\n if (isThenable(handle)) {\n handle.then(\n () => {},\n (error) => onError?.(error),\n );\n }\n return () => controller.abort();\n },\n});\n\nexport const getDefaultWebMcpHost = (): WebMcpHost => {\n const context = resolveModelContext();\n if (!context) {\n return {\n available: false,\n registerTool: () => () => {},\n };\n }\n return createHost(context);\n};\n"],"mappings":";AAyCA,MAAM6B,cAAcC,UAClB,OAAQA,OAAiDC,SAAS;AAIpE,MAAMC,4BAA4D;CAChE,MAAMlB,UACHmB,WAAWC,UAA+BN,gBAC1CK,WAAWE,WAAgCP;CAC9C,IAAI,CAACd,SAAS,OAAOsB,KAAAA;CACrB,IAAI,OAAOtB,QAAQK,iBAAiB,YAAY,OAAOL;CACvDuB,QAAQC,KACN,mGACF;AAEF;AAEA,MAAMC,cAAczB,aAA6C;CAC/DS,WAAW;CACXJ,eAAeK,KAAKC,YAAY;EAC9B,MAAMe,aAAa,IAAIC,gBAAgB;EACvC,MAAMC,SAAS5B,QAAQK,aAAaK,KAAK,EAAET,QAAQyB,WAAWzB,OAAO,CAAC;EACtE,IAAIc,WAAWa,MAAM,GACnBA,OAAOX,WACC,CAAC,IACNL,UAAUD,UAAUC,KAAK,CAC5B;EAEF,aAAac,WAAWG,MAAM;CAChC;AACF;AAEA,MAAaC,6BAAyC;CACpD,MAAM9B,UAAUkB,oBAAoB;CACpC,IAAI,CAAClB,SACH,OAAO;EACLS,WAAW;EACXJ,0BAA0B,CAAC;CAC7B;CAEF,OAAOoB,WAAWzB,OAAO;AAC3B"}
|