@assistant-ui/react 0.15.17 → 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 +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -2
- 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 +41 -22
- 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 +2 -2
- 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/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/messagePart/MessagePartText.d.ts +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 +14 -5
- package/dist/sandbox-host/SandboxHost.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 +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 +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 +4 -4
- package/package.json +12 -12
- package/src/index.ts +13 -0
- 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 +248 -1
- package/src/mcp-apps/app-frame.tsx +51 -5
- package/src/mcp-apps/bridge.ts +7 -2
- package/src/mcp-apps/index.ts +5 -1
- 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/triggerNavigationResource.ts +3 -10
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +7 -6
- package/src/sandbox-host/SandboxHost.test.tsx +120 -2
- package/src/sandbox-host/SandboxHost.tsx +26 -6
- package/src/unstable/useComposerInputHistory.test.tsx +2 -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.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 +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/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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
|
|
3
|
+
import { useEffect, useState } from "@assistant-ui/tap/react-shim";
|
|
4
|
+
//#region src/unstable/useModelContextSnapshot.ts
|
|
5
|
+
/**
|
|
6
|
+
* `getModelContext()` rebuilds its result on every call, so a projection of the
|
|
7
|
+
* fields a consumer observes is held in state and refreshed on notification
|
|
8
|
+
* rather than read during render. Keeping the previous reference for an equal
|
|
9
|
+
* projection is what stops a notification carrying no observable change from
|
|
10
|
+
* invalidating everything derived from it.
|
|
11
|
+
*/
|
|
12
|
+
const useModelContextSnapshot = (aui, enabled, source) => {
|
|
13
|
+
const $ = c(9);
|
|
14
|
+
let t0;
|
|
15
|
+
if ($[0] !== aui || $[1] !== enabled || $[2] !== source) {
|
|
16
|
+
t0 = () => enabled ? source.read(aui) : source.empty;
|
|
17
|
+
$[0] = aui;
|
|
18
|
+
$[1] = enabled;
|
|
19
|
+
$[2] = source;
|
|
20
|
+
$[3] = t0;
|
|
21
|
+
} else t0 = $[3];
|
|
22
|
+
const [snapshot, setSnapshot] = useState(t0);
|
|
23
|
+
let t1;
|
|
24
|
+
let t2;
|
|
25
|
+
if ($[4] !== aui || $[5] !== enabled || $[6] !== source) {
|
|
26
|
+
t1 = () => {
|
|
27
|
+
if (!enabled) return;
|
|
28
|
+
const read = () => {
|
|
29
|
+
const next = source.read(aui);
|
|
30
|
+
setSnapshot((previous) => source.isEqual?.(previous, next) ? previous : next);
|
|
31
|
+
};
|
|
32
|
+
read();
|
|
33
|
+
return source.subscribe(aui, read);
|
|
34
|
+
};
|
|
35
|
+
t2 = [
|
|
36
|
+
aui,
|
|
37
|
+
enabled,
|
|
38
|
+
source
|
|
39
|
+
];
|
|
40
|
+
$[4] = aui;
|
|
41
|
+
$[5] = enabled;
|
|
42
|
+
$[6] = source;
|
|
43
|
+
$[7] = t1;
|
|
44
|
+
$[8] = t2;
|
|
45
|
+
} else {
|
|
46
|
+
t1 = $[7];
|
|
47
|
+
t2 = $[8];
|
|
48
|
+
}
|
|
49
|
+
useEffect(t1, t2);
|
|
50
|
+
return snapshot;
|
|
51
|
+
};
|
|
52
|
+
/** Shallow own-key comparison, for a projection whose values are records. */
|
|
53
|
+
const shallowEqualRecords = (previous, next) => {
|
|
54
|
+
const keys = Object.keys(previous);
|
|
55
|
+
if (keys.length !== Object.keys(next).length) return false;
|
|
56
|
+
return keys.every((key) => Object.hasOwn(next, key) && previous[key] === next[key]);
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
export { shallowEqualRecords, useModelContextSnapshot };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=useModelContextSnapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useModelContextSnapshot.js","names":["c","_c","useEffect","useState","AssistantClient","Unsubscribe","ModelContextSnapshotSource","empty","T","read","aui","subscribe","onChange","isEqual","previous","next","useModelContextSnapshot","enabled","source","$","t0","snapshot","setSnapshot","t1","t2","shallowEqualRecords","Readonly","Record","keys","Object","length","every","key","hasOwn"],"sources":["../../src/unstable/useModelContextSnapshot.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { AssistantClient } from \"@assistant-ui/store\";\nimport type { Unsubscribe } from \"@assistant-ui/core\";\n\n/**\n * Where a snapshot comes from and what counts as a change to it. Declared once\n * per consumer at module scope: the subscription effect keys on this object, so\n * an inline source without an `isEqual` re-reads itself into a render loop.\n */\nexport type ModelContextSnapshotSource<T> = {\n /** Seeds the snapshot when it starts disabled. */\n readonly empty: T;\n /** Projects the fields the consumer observes out of the live model context. */\n read(aui: AssistantClient): T;\n subscribe(aui: AssistantClient, onChange: () => void): Unsubscribe;\n /**\n * An equal pair keeps the previous reference. Omitted by a consumer that\n * re-derives on every notification, where a caller republishing an unchanged\n * tool set is how it asks to be re-run.\n */\n isEqual?(previous: T, next: T): boolean;\n};\n\n/**\n * `getModelContext()` rebuilds its result on every call, so a projection of the\n * fields a consumer observes is held in state and refreshed on notification\n * rather than read during render. Keeping the previous reference for an equal\n * projection is what stops a notification carrying no observable change from\n * invalidating everything derived from it.\n */\nexport const useModelContextSnapshot = <T>(\n aui: AssistantClient,\n enabled: boolean,\n source: ModelContextSnapshotSource<T>,\n): T => {\n const [snapshot, setSnapshot] = useState(() =>\n enabled ? source.read(aui) : source.empty,\n );\n\n useEffect(() => {\n if (!enabled) return undefined;\n const read = () => {\n const next = source.read(aui);\n setSnapshot((previous) =>\n source.isEqual?.(previous, next) ? previous : next,\n );\n };\n read();\n return source.subscribe(aui, read);\n }, [aui, enabled, source]);\n\n return snapshot;\n};\n\n/** Shallow own-key comparison, for a projection whose values are records. */\nexport const shallowEqualRecords = (\n previous: Readonly<Record<string, unknown>>,\n next: Readonly<Record<string, unknown>>,\n): boolean => {\n const keys = Object.keys(previous);\n if (keys.length !== Object.keys(next).length) return false;\n return keys.every(\n (key) => Object.hasOwn(next, key) && previous[key] === next[key],\n );\n};\n"],"mappings":";;;;;;;;;;;AAgCA,MAAagB,2BAA0BN,KAAAO,SAAAC,WAAA;CAAA,MAAAC,IAAAlB,EAAA,CAAA;CAAA,IAAAmB;CAAA,IAAAD,EAAA,OAAAT,OAAAS,EAAA,OAAAF,WAAAE,EAAA,OAAAD,QAAA;EAKIE,WACvCH,UAAUC,OAAMT,KAAMC,GAAkB,IAAXQ,OAAMX;EAAMY,EAAA,KAAAT;EAAAS,EAAA,KAAAF;EAAAE,EAAA,KAAAD;EAAAC,EAAA,KAAAC;CAAA,OAAAA,KAAAD,EAAA;CAD3C,MAAA,CAAAE,UAAAC,eAAgCnB,SAASiB,EAEzC;CAAE,IAAAG;CAAA,IAAAC;CAAA,IAAAL,EAAA,OAAAT,OAAAS,EAAA,OAAAF,WAAAE,EAAA,OAAAD,QAAA;EAEQK,WAAA;GACR,IAAI,CAACN,SAAO;GACZ,MAAAR,aAAa;IACX,MAAAM,OAAaG,OAAMT,KAAMC,GAAG;IAC5BY,aAAYR,aACVI,OAAML,UAAWC,UAAUC,IAAsB,IAAjDD,WAAAC,IACF;GAAC;GAEHN,KAAK;GAAC,OACCS,OAAMP,UAAWD,KAAKD,IAAI;EAAC;EACjCe,KAAA;GAACd;GAAKO;GAASC;EAAM;EAACC,EAAA,KAAAT;EAAAS,EAAA,KAAAF;EAAAE,EAAA,KAAAD;EAAAC,EAAA,KAAAI;EAAAJ,EAAA,KAAAK;CAAA,OAAA;EAAAD,KAAAJ,EAAA;EAAAK,KAAAL,EAAA;CAAA;CAVzBjB,UAAUqB,IAUPC,EAAsB;CAAC,OAEnBH;AAAQ;;AAIjB,MAAaI,uBACXX,UACAC,SACY;CACZ,MAAMa,OAAOC,OAAOD,KAAKd,QAAQ;CACjC,IAAIc,KAAKE,WAAWD,OAAOD,KAAKb,IAAI,CAAC,CAACe,QAAQ,OAAO;CACrD,OAAOF,KAAKG,OACTC,QAAQH,OAAOI,OAAOlB,MAAMiB,GAAG,KAAKlB,SAASkB,SAASjB,KAAKiB,IAC9D;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSlashCommandAdapter.d.ts","names":[],"sources":["../../src/unstable/useSlashCommandAdapter.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"useSlashCommandAdapter.d.ts","names":[],"sources":["../../src/unstable/useSlashCommandAdapter.ts"],"mappings":";;;KAUY;WACD;WACA;WACA;WACA;WACA;;KAGC;WACD,mBAAmB;;WAEnB;;WAEA,UAAU,eAAe;;WAEzB,eAAe;;KAGd;WACD,YAAY,MAAM;WAClB;;;;;;;;;;;;;;;;;;;;;;iBAuBK,gCACd,SAAS;EAET,SAAS;EACT,QAAQ;EACR,UAAU,eAAe;EACzB,eAAe"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { matchesTriggerItemQuery } from "../primitives/composer/trigger/matchesTriggerItemQuery.js";
|
|
2
3
|
import { useLayoutEffect, useMemo, useRef } from "@assistant-ui/tap/react-shim";
|
|
3
4
|
//#region src/unstable/useSlashCommandAdapter.ts
|
|
4
5
|
/**
|
|
@@ -36,7 +37,7 @@ function unstable_useSlashCommandAdapter(options) {
|
|
|
36
37
|
categoryItems: () => [],
|
|
37
38
|
search: (query) => {
|
|
38
39
|
const lower = query.toLowerCase();
|
|
39
|
-
return items.filter((item) =>
|
|
40
|
+
return items.filter((item) => matchesTriggerItemQuery(item, lower));
|
|
40
41
|
}
|
|
41
42
|
}), [items]);
|
|
42
43
|
const action = useMemo(() => ({
|
|
@@ -66,13 +67,6 @@ function toItem(cmd) {
|
|
|
66
67
|
...cmd.icon !== void 0 ? { metadata: { icon: cmd.icon } } : {}
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
|
-
function matchesQuery(item, lower) {
|
|
70
|
-
if (!lower) return true;
|
|
71
|
-
if (item.id.toLowerCase().includes(lower)) return true;
|
|
72
|
-
if (item.label.toLowerCase().includes(lower)) return true;
|
|
73
|
-
if (item.description?.toLowerCase().includes(lower)) return true;
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
70
|
function areTriggerItemsEqual(previous, next) {
|
|
77
71
|
if (!previous || previous.length !== next.length) return false;
|
|
78
72
|
return previous.every((item, index) => {
|
|
@@ -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;cA0CE,eACX,cACA,eAAe,gBACf,kBAAkB,gBACjB"}
|
|
@@ -0,0 +1,110 @@
|
|
|
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 toWebMcpTool = (name, getTool, lifecycleSignal) => ({
|
|
68
|
+
name,
|
|
69
|
+
description: getTool().description ?? "",
|
|
70
|
+
inputSchema: toWebMcpInputSchema(getTool()),
|
|
71
|
+
execute: async (rawArgs, context) => {
|
|
72
|
+
if (lifecycleSignal?.aborted) return errorResult(`Tool "${name}" is no longer registered`);
|
|
73
|
+
const tool = getTool();
|
|
74
|
+
const args = rawArgs ?? {};
|
|
75
|
+
const toolCallId = generateId();
|
|
76
|
+
try {
|
|
77
|
+
const callerSignal = context?.signal;
|
|
78
|
+
const abortSignal = !callerSignal ? lifecycleSignal : !lifecycleSignal ? callerSignal : AbortSignal.any([callerSignal, lifecycleSignal]);
|
|
79
|
+
let executeFn = tool.execute;
|
|
80
|
+
if (isStandardSchema(tool.parameters)) {
|
|
81
|
+
let validation = tool.parameters["~standard"].validate(args);
|
|
82
|
+
validation = await validation;
|
|
83
|
+
if (validation.issues) {
|
|
84
|
+
const issues = validation.issues;
|
|
85
|
+
executeFn = tool.experimental_onSchemaValidationError ?? (() => {
|
|
86
|
+
throw new Error(`Function parameter validation failed. ${JSON.stringify(issues)}`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (abortSignal?.aborted) return errorResult("Tool execution was cancelled.");
|
|
91
|
+
if (!executeFn) return errorResult(`Tool "${name}" has no client-side implementation.`);
|
|
92
|
+
const result = await executeFn(args, {
|
|
93
|
+
toolCallId,
|
|
94
|
+
abortSignal: abortSignal ?? new AbortController().signal,
|
|
95
|
+
human: () => Promise.reject(/* @__PURE__ */ new Error("human input not supported in WebMCP context"))
|
|
96
|
+
});
|
|
97
|
+
return await toMcpContent(result, {
|
|
98
|
+
tool,
|
|
99
|
+
toolCallId,
|
|
100
|
+
args
|
|
101
|
+
});
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return errorResult(e instanceof Error ? e.message : String(e));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
//#endregion
|
|
108
|
+
export { defaultWebMcpFilter, toMcpContent, toWebMcpInputSchema, toWebMcpTool };
|
|
109
|
+
|
|
110
|
+
//# 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","toWebMcpTool","name","getTool","lifecycleSignal","AbortSignal","description","inputSchema","rawArgs","context","aborted","callerSignal","signal","abortSignal","any","executeFn","validation","experimental_onSchemaValidationError","Error","AbortController","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\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 try {\n const callerSignal = context?.signal;\n const abortSignal = !callerSignal\n ? lifecycleSignal\n : !lifecycleSignal\n ? callerSignal\n : AbortSignal.any([callerSignal, lifecycleSignal]);\n let executeFn = tool.execute;\n if (isStandardSchema(tool.parameters)) {\n let validation = tool.parameters[\"~standard\"].validate(args);\n validation = await validation;\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 }\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":";;KAcY;EACV,UAAU,cAAc,MAAM;;KAGpB;EACV;EACA;;;;;;;;;;cAwGW,6BACX,UAAS,mCACR"}
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
//#region src/unstable/webmcp/useWebMcpProvider.ts
|
|
11
|
+
const EMPTY_NAMES = Object.freeze([]);
|
|
12
|
+
const EMPTY_TOOLS = Object.freeze({});
|
|
13
|
+
const warned = /* @__PURE__ */ new WeakSet();
|
|
14
|
+
const signatures = /* @__PURE__ */ new WeakMap();
|
|
15
|
+
const signatureOf = (tool) => {
|
|
16
|
+
const cached = signatures.get(tool);
|
|
17
|
+
if (cached && cached.description === tool.description) return cached.signature;
|
|
18
|
+
const signature = `${tool.description ?? ""}\u0000${JSON.stringify(toWebMcpInputSchema(tool))}`;
|
|
19
|
+
signatures.set(tool, {
|
|
20
|
+
description: tool.description,
|
|
21
|
+
signature
|
|
22
|
+
});
|
|
23
|
+
return signature;
|
|
24
|
+
};
|
|
25
|
+
const NO_SUBSCRIPTION = () => {};
|
|
26
|
+
const modelContextToolSource = {
|
|
27
|
+
empty: EMPTY_TOOLS,
|
|
28
|
+
read: (aui) => aui.modelContext.getModelContext().tools ?? EMPTY_TOOLS,
|
|
29
|
+
subscribe: (aui, onChange) => aui.modelContext.subscribe?.(onChange) ?? NO_SUBSCRIPTION
|
|
30
|
+
};
|
|
31
|
+
const useStableNames = (names) => {
|
|
32
|
+
const [cell] = useState(() => ({ names: EMPTY_NAMES }));
|
|
33
|
+
const next = names.filter((name) => name !== null).sort();
|
|
34
|
+
const previous = cell.names;
|
|
35
|
+
if (previous.length === next.length && previous.every((name_0, index) => name_0 === next[index])) return previous;
|
|
36
|
+
cell.names = next;
|
|
37
|
+
return next;
|
|
38
|
+
};
|
|
39
|
+
const useWebMcpRegistry = (t0) => {
|
|
40
|
+
const $ = c(4);
|
|
41
|
+
const { aui, host, filter } = t0;
|
|
42
|
+
const tools = useModelContextSnapshot(aui, host.available, modelContextToolSource);
|
|
43
|
+
let elements;
|
|
44
|
+
if ($[0] !== filter || $[1] !== host || $[2] !== tools) {
|
|
45
|
+
elements = [];
|
|
46
|
+
for (const [name, tool] of Object.entries(tools)) try {
|
|
47
|
+
if (!filter(name, tool)) continue;
|
|
48
|
+
const signature = signatureOf(tool);
|
|
49
|
+
elements.push(withKey(name, WebMcpRegistrationResource({
|
|
50
|
+
host,
|
|
51
|
+
name,
|
|
52
|
+
signature,
|
|
53
|
+
tool
|
|
54
|
+
}), [
|
|
55
|
+
host,
|
|
56
|
+
name,
|
|
57
|
+
signature,
|
|
58
|
+
tool
|
|
59
|
+
]));
|
|
60
|
+
} catch (t1) {
|
|
61
|
+
const error = t1;
|
|
62
|
+
if (warned.has(tool)) continue;
|
|
63
|
+
warned.add(tool);
|
|
64
|
+
console.warn(`[assistant-ui] Skipping WebMCP registration for tool "${name}": filter or schema conversion failed.`, error);
|
|
65
|
+
}
|
|
66
|
+
$[0] = filter;
|
|
67
|
+
$[1] = host;
|
|
68
|
+
$[2] = tools;
|
|
69
|
+
$[3] = elements;
|
|
70
|
+
} else elements = $[3];
|
|
71
|
+
return useStableNames(useResources(elements));
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Publishes the frontend tools in the model context to a WebMCP-capable
|
|
75
|
+
* browser, so the user's own browser agent can call them.
|
|
76
|
+
*
|
|
77
|
+
* Returns `status: "unsupported"` when the page exposes no
|
|
78
|
+
* `document.modelContext` (or `navigator.modelContext`), and the sorted names
|
|
79
|
+
* of the tools currently registered with the host.
|
|
80
|
+
*/
|
|
81
|
+
const unstable_useWebMcpProvider = (options = {}) => {
|
|
82
|
+
const aui = useAui();
|
|
83
|
+
const [host] = useState(getDefaultWebMcpHost);
|
|
84
|
+
const filter = options.filter ?? defaultWebMcpFilter;
|
|
85
|
+
const root = useTapRoot(function WebMcpProviderRoot() {
|
|
86
|
+
return useWebMcpRegistry({
|
|
87
|
+
aui,
|
|
88
|
+
host,
|
|
89
|
+
filter
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
const registeredToolNames = useSyncExternalStore(root.subscribe, root.getValue, () => EMPTY_NAMES);
|
|
93
|
+
const [published, setPublished] = useState(false);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (host.available) setPublished(true);
|
|
96
|
+
}, [host]);
|
|
97
|
+
return {
|
|
98
|
+
status: published ? "active" : "unsupported",
|
|
99
|
+
registeredToolNames
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
//#endregion
|
|
103
|
+
export { unstable_useWebMcpProvider };
|
|
104
|
+
|
|
105
|
+
//# 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","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","length","every","index","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 {\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 (\n previous.length === next.length &&\n previous.every((name, index) => name === next[index])\n ) {\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":";;;;;;;;;;AAuBA,MAAMyB,cAAiCC,OAAOC,OAAO,CAAA,CAAE;AACvD,MAAMC,cAA8CF,OAAOC,OAAO,CAAC,CAAC;AAMpE,MAAMG,yBAAS,IAAIC,QAAwB;AAE3C,MAAMC,6BAAa,IAAIC,QAGrB;AAEF,MAAMG,eAAef,SAAyB;CAC5C,MAAMgB,SAASL,WAAWM,IAAIjB,IAAI;CAClC,IAAIgB,UAAUA,OAAOH,gBAAgBb,KAAKa,aACxC,OAAOG,OAAOF;CAChB,MAAMA,YAAY,GAAGd,KAAKa,eAAe,GAAE,QAASK,KAAKC,UACvD1B,oBAAoBO,IAAI,CAC1B;CACAW,WAAWS,IAAIpB,MAAM;EAAEa,aAAab,KAAKa;EAAaC;CAAU,CAAC;CACjE,OAAOA;AACT;AAEA,MAAMO,wBAAwB,CAAC;AAK/B,MAAMC,yBAEF;CACFC,OAAOhB;CACPiB,OAAOC,QAAQA,IAAIC,aAAaC,gBAAgB,CAAC,CAACC,SAASrB;CAC3DsB,YAAYJ,KAAKK,aACfL,IAAIC,aAAaG,YAAYC,QAAQ,KAAKT;AAC9C;AAEA,MAAMU,kBAAkBC,UAAsC;CAC5D,MAAM,CAACC,QAAQnD,gBAAgB,EAAEkD,OAAO5B,YAAY,EAAE;CACtD,MAAM8B,OAAOF,MAAMlC,QAAQC,SAAyBA,SAAS,IAAI,CAAC,CAACoC,KAAK;CACxE,MAAMC,WAAWH,KAAKD;CACtB,IACEI,SAASC,WAAWH,KAAKG,UACzBD,SAASE,OAAOvC,QAAMwC,UAAUxC,WAASmC,KAAKK,MAAM,GAEpD,OAAOH;CAETH,KAAKD,QAAQE;CACb,OAAOA;AACT;AAEA,MAAMM,qBAAoBC,OAAA;CAAA,MAAAC,IAAA9D,EAAA,CAAA;CAAC,MAAA,EAAA6C,KAAAkB,MAAA7C,WAAA2C;CASzB,MAAAb,QAAcjC,wBACZ8B,KACAkB,KAAIC,WACJtB,sBACF;CAAE,IAAAuB;CAAA,IAAAH,EAAA,OAAA5C,UAAA4C,EAAA,OAAAC,QAAAD,EAAA,OAAAd,OAAA;EAEFiB,WAAiB,CAAA;EACjB,KAAK,MAAA,CAAA9C,MAAAC,SAAsBK,OAAMyC,QAASlB,KAAK,GAC7C,IAAA;GACE,IAAI,CAAC9B,OAAOC,MAAMC,IAAI,GAAG;GACzB,MAAAc,YAAkBC,YAAYf,IAAI;GAClC6C,SAAQE,KACN7D,QACEa,MACAL,2BAA2B;IAAAiD;IAAA5C;IAAAe;IAAAd;GAA8B,CAAC,GAC1D;IAAC2C;IAAM5C;IAAMe;IAAWd;GAAI,CAC9B,CACF;EAAC,SAAAgD,IAAA;GACMC,MAAAA,QAAAA;GACP,IAAIxC,OAAMyC,IAAKlD,IAAI,GAAG;GACtBS,OAAM0C,IAAKnD,IAAI;GACfoD,QAAOC,KACL,yDAAyDtD,KAAI,yCAC7DkD,KACF;EAAC;EAEJP,EAAA,KAAA5C;EAAA4C,EAAA,KAAAC;EAAAD,EAAA,KAAAd;EAAAc,EAAA,KAAAG;CAAA,OAAAA,WAAAH,EAAA;CAAA,OAEMX,eAAe/C,aAAa6D,QAAQ,CAAC;AAAC;;;;;;;;;AAW/C,MAAaS,8BACXC,UAA0C,CAAC,MACT;CAClC,MAAM9B,MAAMtC,OAAO;CACnB,MAAM,CAACwD,QAAQ7D,SAASQ,oBAAoB;CAC5C,MAAMQ,SAASyD,QAAQzD,UAAUN;CAEjC,MAAMgE,OAAOvE,WAAW,SAASwE,qBAAqB;EACpD,OAAOjB,kBAAkB;GAAEf;GAAKkB;GAAM7C;EAAO,CAAC;CAChD,CAAC;CACD,MAAMK,sBAAsBpB,qBAC1ByE,KAAK3B,WACL2B,KAAKE,gBACCtD,WACR;CAEA,MAAM,CAACuD,WAAWC,gBAAgB9E,SAAS,KAAK;CAChDD,gBAAgB;EACd,IAAI8D,KAAKC,WAAWgB,aAAa,IAAI;CACvC,GAAG,CAACjB,IAAI,CAAC;CAET,OAAO;EACLzC,QAAQyD,YAAY,WAAW;EAC/BxD;CACF;AACF"}
|
|
@@ -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"}
|