@jay-framework/webmcp-plugin 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,327 @@
1
+ import { makeJayInit } from "@jay-framework/fullstack-component";
2
+ function toKebab(s) {
3
+ return s.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
4
+ }
5
+ function toHumanReadable(s) {
6
+ return s.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
7
+ }
8
+ function parseCoordinate(s) {
9
+ return s.split("/");
10
+ }
11
+ function jsonResult(label, data) {
12
+ return {
13
+ content: [{ type: "text", text: `${label}
14
+ ${JSON.stringify(data, null, 2)}` }]
15
+ };
16
+ }
17
+ function errorResult(text) {
18
+ return {
19
+ content: [{ type: "text", text: `Error: ${text}` }],
20
+ isError: true
21
+ };
22
+ }
23
+ function withLogging(tool) {
24
+ const original = tool.execute;
25
+ return {
26
+ ...tool,
27
+ execute(params, agent) {
28
+ Object.keys(params).length > 0 ? " " + JSON.stringify(params) : "";
29
+ return original(params, agent);
30
+ }
31
+ };
32
+ }
33
+ function isCheckable(element) {
34
+ return element instanceof HTMLInputElement && (element.type === "checkbox" || element.type === "radio");
35
+ }
36
+ function setElementValue(element, value) {
37
+ if (isCheckable(element)) {
38
+ element.checked = value === "true";
39
+ } else {
40
+ element.value = value;
41
+ }
42
+ }
43
+ function getValueEventTypes(registeredEvents) {
44
+ const result = [];
45
+ if (registeredEvents.includes("input"))
46
+ result.push("input");
47
+ if (registeredEvents.includes("change"))
48
+ result.push("change");
49
+ if (result.length > 0)
50
+ return result;
51
+ return registeredEvents.length > 0 ? [registeredEvents[0]] : ["input"];
52
+ }
53
+ function getSelectOptions(element) {
54
+ if (!(element instanceof HTMLSelectElement))
55
+ return void 0;
56
+ const options = [];
57
+ for (const opt of Array.from(element.options)) {
58
+ options.push(opt.value);
59
+ }
60
+ return options;
61
+ }
62
+ function serializeInteractions(interactions) {
63
+ return interactions.map((group) => ({
64
+ refName: group.refName,
65
+ ...group.description ? { description: group.description } : {},
66
+ items: group.items.map((i) => {
67
+ const options = getSelectOptions(i.element);
68
+ const checkable = isCheckable(i.element);
69
+ return {
70
+ coordinate: i.coordinate.join("/"),
71
+ elementType: i.element.constructor.name,
72
+ events: i.events,
73
+ ...options ? { options } : {},
74
+ ...checkable ? { inputType: i.element.type } : {}
75
+ };
76
+ })
77
+ }));
78
+ }
79
+ function makeGetPageStateTool(automation) {
80
+ return withLogging({
81
+ name: "get-page-state",
82
+ description: `Get current page state (ViewState) — all data displayed on the page as JSON. Arrays in the ViewState correspond to forEach lists in the UI. Each array item has an ID field (the trackBy key) that appears as the first segment of interaction coordinates. For example, if ViewState has items: [{id: "item-1", name: "Mouse"}], then "item-1/removeBtn" targets that item's remove button.`,
83
+ inputSchema: {
84
+ type: "object",
85
+ properties: {}
86
+ },
87
+ execute: () => {
88
+ const { viewState } = automation.getPageState();
89
+ return jsonResult("Current page state:", viewState);
90
+ }
91
+ });
92
+ }
93
+ function makeListInteractionsTool(automation) {
94
+ return withLogging({
95
+ name: "list-interactions",
96
+ description: 'List all interactive elements on the page, grouped by ref name. Each group has items with coordinate strings that identify specific elements. Single-segment coordinates (e.g. "addBtn") are standalone elements. Multi-segment coordinates (e.g. "item-1/removeBtn") are elements inside a list — the first segments identify the list item, the last segment is the element name.',
97
+ inputSchema: {
98
+ type: "object",
99
+ properties: {}
100
+ },
101
+ execute: () => {
102
+ const { interactions } = automation.getPageState();
103
+ return jsonResult("Available interactions:", serializeInteractions(interactions));
104
+ }
105
+ });
106
+ }
107
+ function makeTriggerInteractionTool(automation) {
108
+ return withLogging({
109
+ name: "trigger-interaction",
110
+ description: 'Trigger an event on a UI element by its coordinate string. Use list-interactions to discover available coordinates. Default event is "click".',
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ coordinate: {
115
+ type: "string",
116
+ description: 'Element coordinate (e.g. "addBtn" or "item-1/removeBtn")'
117
+ },
118
+ event: {
119
+ type: "string",
120
+ description: 'Event type to trigger (default: "click")'
121
+ }
122
+ },
123
+ required: ["coordinate"]
124
+ },
125
+ execute: (params) => {
126
+ const coord = parseCoordinate(params.coordinate);
127
+ const event = params.event || "click";
128
+ try {
129
+ automation.triggerEvent(event, coord);
130
+ return jsonResult(
131
+ `Triggered "${event}" on ${params.coordinate}`,
132
+ automation.getPageState().viewState
133
+ );
134
+ } catch (e) {
135
+ return errorResult(e.message);
136
+ }
137
+ }
138
+ });
139
+ }
140
+ function makeFillInputTool(automation) {
141
+ return withLogging({
142
+ name: "fill-input",
143
+ description: 'Set a value on an input, textarea, or select element and trigger an update event. For checkbox/radio inputs, pass "true" or "false" to toggle the checked state. Use list-interactions to find input coordinates.',
144
+ inputSchema: {
145
+ type: "object",
146
+ properties: {
147
+ coordinate: {
148
+ type: "string",
149
+ description: 'Element coordinate (e.g. "nameInput" or "item-1/quantityInput")'
150
+ },
151
+ value: {
152
+ type: "string",
153
+ description: 'The value to set (for checkbox/radio: "true" or "false")'
154
+ }
155
+ },
156
+ required: ["coordinate", "value"]
157
+ },
158
+ execute: (params) => {
159
+ const coord = parseCoordinate(params.coordinate);
160
+ const value = params.value;
161
+ try {
162
+ const instance = automation.getInteraction(coord);
163
+ if (!instance) {
164
+ return errorResult(`No element found at coordinate: ${coord.join("/")}`);
165
+ }
166
+ setElementValue(instance.element, value);
167
+ for (const evt of getValueEventTypes(instance.events)) {
168
+ automation.triggerEvent(evt, coord);
169
+ }
170
+ return jsonResult(
171
+ `Set value "${value}" on ${params.coordinate}`,
172
+ automation.getPageState().viewState
173
+ );
174
+ } catch (e) {
175
+ return errorResult(e.message);
176
+ }
177
+ }
178
+ });
179
+ }
180
+ const FILLABLE_TYPES = /* @__PURE__ */ new Set(["HTMLInputElement", "HTMLTextAreaElement", "HTMLSelectElement"]);
181
+ function buildSemanticTools(automation) {
182
+ const { interactions } = automation.getPageState();
183
+ const tools = [];
184
+ for (const group of interactions) {
185
+ const tool = makeSemanticTool(group, automation);
186
+ if (tool) {
187
+ tools.push(tool);
188
+ }
189
+ }
190
+ return tools;
191
+ }
192
+ function makeSemanticTool(group, automation) {
193
+ const sample = group.items[0];
194
+ if (!sample)
195
+ return null;
196
+ const elementType = sample.element.constructor.name;
197
+ const isFillable = FILLABLE_TYPES.has(elementType);
198
+ const isSelect = elementType === "HTMLSelectElement";
199
+ const checkable = isCheckable(sample.element);
200
+ const isForEach = group.items.length > 1 || sample.coordinate.length > 1;
201
+ const prefix = checkable ? "toggle" : isFillable ? "fill" : "click";
202
+ const toolName = `${prefix}-${toKebab(group.refName)}`;
203
+ const description = group.description || `${checkable ? "Toggle" : isFillable ? "Fill" : "Click"} ${toHumanReadable(group.refName)}${isForEach ? " for a specific item" : ""}`;
204
+ const properties = {};
205
+ const required = [];
206
+ if (isForEach) {
207
+ const coordStrings = group.items.map((i) => i.coordinate.join("/"));
208
+ properties.coordinate = {
209
+ type: "string",
210
+ description: "Item coordinate",
211
+ enum: coordStrings
212
+ };
213
+ required.push("coordinate");
214
+ }
215
+ if (isFillable) {
216
+ if (checkable) {
217
+ properties.value = {
218
+ type: "string",
219
+ description: "Checked state",
220
+ enum: ["true", "false"]
221
+ };
222
+ } else {
223
+ const selectOptions = isSelect ? getSelectOptions(sample.element) : void 0;
224
+ properties.value = {
225
+ type: "string",
226
+ description: isSelect ? "Value to select" : "Value to set",
227
+ ...selectOptions ? { enum: selectOptions } : {}
228
+ };
229
+ }
230
+ required.push("value");
231
+ }
232
+ return withLogging({
233
+ name: toolName,
234
+ description,
235
+ inputSchema: { type: "object", properties, required },
236
+ execute: (params) => {
237
+ const coord = isForEach ? params.coordinate.split("/") : [group.refName];
238
+ try {
239
+ if (isFillable) {
240
+ const instance = automation.getInteraction(coord);
241
+ if (!instance)
242
+ return errorResult(`Element not found: ${coord.join("/")}`);
243
+ setElementValue(instance.element, params.value);
244
+ for (const evt of getValueEventTypes(instance.events)) {
245
+ automation.triggerEvent(evt, coord);
246
+ }
247
+ } else {
248
+ automation.triggerEvent("click", coord);
249
+ }
250
+ return jsonResult("Done", automation.getPageState().viewState);
251
+ } catch (e) {
252
+ return errorResult(e.message);
253
+ }
254
+ }
255
+ });
256
+ }
257
+ function setupWebMCP(automation) {
258
+ if (!navigator.modelContext) {
259
+ console.warn("[WebMCP] navigator.modelContext not available — skipping registration");
260
+ return () => {
261
+ };
262
+ }
263
+ const mc = navigator.modelContext;
264
+ const genericTools = [
265
+ makeGetPageStateTool(automation),
266
+ makeListInteractionsTool(automation),
267
+ makeTriggerInteractionTool(automation),
268
+ makeFillInputTool(automation)
269
+ ];
270
+ for (const tool of genericTools) {
271
+ mc.registerTool(tool);
272
+ }
273
+ let semanticTools = buildAndRegisterSemanticTools(mc, automation);
274
+ let lastKey = interactionKey(automation);
275
+ const unsubscribe = automation.onStateChange(() => {
276
+ const newKey = interactionKey(automation);
277
+ if (newKey !== lastKey) {
278
+ semanticTools.forEach((t) => mc.unregisterTool(t.name));
279
+ semanticTools = buildAndRegisterSemanticTools(mc, automation);
280
+ lastKey = newKey;
281
+ }
282
+ });
283
+ const allTools = () => [...genericTools, ...semanticTools];
284
+ window.webmcp = {
285
+ tools() {
286
+ const tools = allTools();
287
+ console.table(tools.map((t) => ({ name: t.name, description: t.description })));
288
+ return tools;
289
+ }
290
+ };
291
+ console.log(
292
+ `[WebMCP] Registered ${genericTools.length + semanticTools.length} tools — type webmcp.tools() to list`
293
+ );
294
+ return () => {
295
+ unsubscribe();
296
+ genericTools.forEach((t) => mc.unregisterTool(t.name));
297
+ semanticTools.forEach((t) => mc.unregisterTool(t.name));
298
+ delete window.webmcp;
299
+ };
300
+ }
301
+ function buildAndRegisterSemanticTools(mc, automation) {
302
+ const tools = buildSemanticTools(automation);
303
+ for (const tool of tools) {
304
+ mc.registerTool(tool);
305
+ }
306
+ return tools;
307
+ }
308
+ function interactionKey(automation) {
309
+ return automation.getPageState().interactions.map(
310
+ (g) => `${g.refName}:${g.items.map((i) => i.coordinate.join("/")).join(",")}`
311
+ ).join("|");
312
+ }
313
+ const init = makeJayInit().withClient(() => {
314
+ setTimeout(() => {
315
+ const automation = window.__jay?.automation;
316
+ if (automation) {
317
+ const cleanup = setupWebMCP(automation);
318
+ window.addEventListener("beforeunload", cleanup);
319
+ } else {
320
+ console.warn("[WebMCP] window.__jay.automation not available — skipping");
321
+ }
322
+ }, 0);
323
+ });
324
+ export {
325
+ init,
326
+ setupWebMCP
327
+ };
@@ -0,0 +1,84 @@
1
+ import * as _jay_framework_fullstack_component from '@jay-framework/fullstack-component';
2
+ import { AutomationAPI } from '@jay-framework/runtime-automation';
3
+
4
+ /**
5
+ * WebMCP plugin init — client-only.
6
+ *
7
+ * Uses setTimeout(0) to defer setup until after the current task completes:
8
+ * 1. All plugin client inits (each `await`ed, creating microtasks)
9
+ * 2. Page component is created and mounted
10
+ * 3. wrapWithAutomation() is called
11
+ * 4. window.__jay.automation is set
12
+ *
13
+ * Why setTimeout and not queueMicrotask:
14
+ * Plugin inits are `await`ed in the generated script. `await undefined` yields
15
+ * to the microtask queue, so a queueMicrotask callback runs BEFORE the rest of
16
+ * the script (component creation, automation setup). setTimeout(0) schedules on
17
+ * the macrotask queue, which runs after all synchronous code and microtasks.
18
+ */
19
+ declare const init: _jay_framework_fullstack_component.JayInit<void>;
20
+
21
+ /**
22
+ * Type declarations for the WebMCP API (navigator.modelContext)
23
+ *
24
+ * Based on Chrome Canary's actual ModelContext API:
25
+ * - clearContext
26
+ * - provideContext
27
+ * - registerTool
28
+ * - unregisterTool
29
+ */
30
+ interface ToolInputSchema {
31
+ type: 'object';
32
+ properties: Record<string, {
33
+ type: string;
34
+ description?: string;
35
+ enum?: string[];
36
+ }>;
37
+ required?: string[];
38
+ }
39
+ interface ToolContentItem {
40
+ type: 'text' | 'image' | 'resource';
41
+ text?: string;
42
+ data?: string;
43
+ mimeType?: string;
44
+ }
45
+ interface ToolResult {
46
+ content: ToolContentItem[];
47
+ isError?: boolean;
48
+ }
49
+ interface Agent {
50
+ requestUserInteraction: <T>(callback: () => T | Promise<T>) => Promise<T>;
51
+ }
52
+ interface ToolDescriptor {
53
+ name: string;
54
+ description: string;
55
+ inputSchema: ToolInputSchema;
56
+ execute: (params: Record<string, unknown>, agent: Agent) => ToolResult | Promise<ToolResult>;
57
+ }
58
+ interface ModelContextContainer {
59
+ /** Clear all registered tools and context */
60
+ clearContext(): void;
61
+ /** Set all tools at once (replaces previously registered tools) */
62
+ provideContext(params: {
63
+ tools: ToolDescriptor[];
64
+ }): void;
65
+ /** Register an individual tool (additive) */
66
+ registerTool(tool: ToolDescriptor): void;
67
+ /** Unregister a previously registered tool by name */
68
+ unregisterTool(name: string): void;
69
+ }
70
+ declare global {
71
+ interface Navigator {
72
+ modelContext?: ModelContextContainer;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Main entry point. Registers all WebMCP tools derived from the given
78
+ * AutomationAPI instance.
79
+ *
80
+ * @returns Cleanup function that unregisters everything.
81
+ */
82
+ declare function setupWebMCP(automation: AutomationAPI): () => void;
83
+
84
+ export { type ModelContextContainer, type ToolDescriptor, type ToolInputSchema, type ToolResult, init, setupWebMCP };
package/dist/index.js ADDED
@@ -0,0 +1,317 @@
1
+ import { makeJayInit } from "@jay-framework/fullstack-component";
2
+ const init = makeJayInit();
3
+ function toKebab(s) {
4
+ return s.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
5
+ }
6
+ function toHumanReadable(s) {
7
+ return s.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
8
+ }
9
+ function parseCoordinate(s) {
10
+ return s.split("/");
11
+ }
12
+ function jsonResult(label, data) {
13
+ return {
14
+ content: [{ type: "text", text: `${label}
15
+ ${JSON.stringify(data, null, 2)}` }]
16
+ };
17
+ }
18
+ function errorResult(text) {
19
+ return {
20
+ content: [{ type: "text", text: `Error: ${text}` }],
21
+ isError: true
22
+ };
23
+ }
24
+ function withLogging(tool) {
25
+ const original = tool.execute;
26
+ return {
27
+ ...tool,
28
+ execute(params, agent) {
29
+ Object.keys(params).length > 0 ? " " + JSON.stringify(params) : "";
30
+ return original(params, agent);
31
+ }
32
+ };
33
+ }
34
+ function isCheckable(element) {
35
+ return element instanceof HTMLInputElement && (element.type === "checkbox" || element.type === "radio");
36
+ }
37
+ function setElementValue(element, value) {
38
+ if (isCheckable(element)) {
39
+ element.checked = value === "true";
40
+ } else {
41
+ element.value = value;
42
+ }
43
+ }
44
+ function getValueEventTypes(registeredEvents) {
45
+ const result = [];
46
+ if (registeredEvents.includes("input"))
47
+ result.push("input");
48
+ if (registeredEvents.includes("change"))
49
+ result.push("change");
50
+ if (result.length > 0)
51
+ return result;
52
+ return registeredEvents.length > 0 ? [registeredEvents[0]] : ["input"];
53
+ }
54
+ function getSelectOptions(element) {
55
+ if (!(element instanceof HTMLSelectElement))
56
+ return void 0;
57
+ const options = [];
58
+ for (const opt of Array.from(element.options)) {
59
+ options.push(opt.value);
60
+ }
61
+ return options;
62
+ }
63
+ function serializeInteractions(interactions) {
64
+ return interactions.map((group) => ({
65
+ refName: group.refName,
66
+ ...group.description ? { description: group.description } : {},
67
+ items: group.items.map((i) => {
68
+ const options = getSelectOptions(i.element);
69
+ const checkable = isCheckable(i.element);
70
+ return {
71
+ coordinate: i.coordinate.join("/"),
72
+ elementType: i.element.constructor.name,
73
+ events: i.events,
74
+ ...options ? { options } : {},
75
+ ...checkable ? { inputType: i.element.type } : {}
76
+ };
77
+ })
78
+ }));
79
+ }
80
+ function makeGetPageStateTool(automation) {
81
+ return withLogging({
82
+ name: "get-page-state",
83
+ description: `Get current page state (ViewState) — all data displayed on the page as JSON. Arrays in the ViewState correspond to forEach lists in the UI. Each array item has an ID field (the trackBy key) that appears as the first segment of interaction coordinates. For example, if ViewState has items: [{id: "item-1", name: "Mouse"}], then "item-1/removeBtn" targets that item's remove button.`,
84
+ inputSchema: {
85
+ type: "object",
86
+ properties: {}
87
+ },
88
+ execute: () => {
89
+ const { viewState } = automation.getPageState();
90
+ return jsonResult("Current page state:", viewState);
91
+ }
92
+ });
93
+ }
94
+ function makeListInteractionsTool(automation) {
95
+ return withLogging({
96
+ name: "list-interactions",
97
+ description: 'List all interactive elements on the page, grouped by ref name. Each group has items with coordinate strings that identify specific elements. Single-segment coordinates (e.g. "addBtn") are standalone elements. Multi-segment coordinates (e.g. "item-1/removeBtn") are elements inside a list — the first segments identify the list item, the last segment is the element name.',
98
+ inputSchema: {
99
+ type: "object",
100
+ properties: {}
101
+ },
102
+ execute: () => {
103
+ const { interactions } = automation.getPageState();
104
+ return jsonResult("Available interactions:", serializeInteractions(interactions));
105
+ }
106
+ });
107
+ }
108
+ function makeTriggerInteractionTool(automation) {
109
+ return withLogging({
110
+ name: "trigger-interaction",
111
+ description: 'Trigger an event on a UI element by its coordinate string. Use list-interactions to discover available coordinates. Default event is "click".',
112
+ inputSchema: {
113
+ type: "object",
114
+ properties: {
115
+ coordinate: {
116
+ type: "string",
117
+ description: 'Element coordinate (e.g. "addBtn" or "item-1/removeBtn")'
118
+ },
119
+ event: {
120
+ type: "string",
121
+ description: 'Event type to trigger (default: "click")'
122
+ }
123
+ },
124
+ required: ["coordinate"]
125
+ },
126
+ execute: (params) => {
127
+ const coord = parseCoordinate(params.coordinate);
128
+ const event = params.event || "click";
129
+ try {
130
+ automation.triggerEvent(event, coord);
131
+ return jsonResult(
132
+ `Triggered "${event}" on ${params.coordinate}`,
133
+ automation.getPageState().viewState
134
+ );
135
+ } catch (e) {
136
+ return errorResult(e.message);
137
+ }
138
+ }
139
+ });
140
+ }
141
+ function makeFillInputTool(automation) {
142
+ return withLogging({
143
+ name: "fill-input",
144
+ description: 'Set a value on an input, textarea, or select element and trigger an update event. For checkbox/radio inputs, pass "true" or "false" to toggle the checked state. Use list-interactions to find input coordinates.',
145
+ inputSchema: {
146
+ type: "object",
147
+ properties: {
148
+ coordinate: {
149
+ type: "string",
150
+ description: 'Element coordinate (e.g. "nameInput" or "item-1/quantityInput")'
151
+ },
152
+ value: {
153
+ type: "string",
154
+ description: 'The value to set (for checkbox/radio: "true" or "false")'
155
+ }
156
+ },
157
+ required: ["coordinate", "value"]
158
+ },
159
+ execute: (params) => {
160
+ const coord = parseCoordinate(params.coordinate);
161
+ const value = params.value;
162
+ try {
163
+ const instance = automation.getInteraction(coord);
164
+ if (!instance) {
165
+ return errorResult(`No element found at coordinate: ${coord.join("/")}`);
166
+ }
167
+ setElementValue(instance.element, value);
168
+ for (const evt of getValueEventTypes(instance.events)) {
169
+ automation.triggerEvent(evt, coord);
170
+ }
171
+ return jsonResult(
172
+ `Set value "${value}" on ${params.coordinate}`,
173
+ automation.getPageState().viewState
174
+ );
175
+ } catch (e) {
176
+ return errorResult(e.message);
177
+ }
178
+ }
179
+ });
180
+ }
181
+ const FILLABLE_TYPES = /* @__PURE__ */ new Set(["HTMLInputElement", "HTMLTextAreaElement", "HTMLSelectElement"]);
182
+ function buildSemanticTools(automation) {
183
+ const { interactions } = automation.getPageState();
184
+ const tools = [];
185
+ for (const group of interactions) {
186
+ const tool = makeSemanticTool(group, automation);
187
+ if (tool) {
188
+ tools.push(tool);
189
+ }
190
+ }
191
+ return tools;
192
+ }
193
+ function makeSemanticTool(group, automation) {
194
+ const sample = group.items[0];
195
+ if (!sample)
196
+ return null;
197
+ const elementType = sample.element.constructor.name;
198
+ const isFillable = FILLABLE_TYPES.has(elementType);
199
+ const isSelect = elementType === "HTMLSelectElement";
200
+ const checkable = isCheckable(sample.element);
201
+ const isForEach = group.items.length > 1 || sample.coordinate.length > 1;
202
+ const prefix = checkable ? "toggle" : isFillable ? "fill" : "click";
203
+ const toolName = `${prefix}-${toKebab(group.refName)}`;
204
+ const description = group.description || `${checkable ? "Toggle" : isFillable ? "Fill" : "Click"} ${toHumanReadable(group.refName)}${isForEach ? " for a specific item" : ""}`;
205
+ const properties = {};
206
+ const required = [];
207
+ if (isForEach) {
208
+ const coordStrings = group.items.map((i) => i.coordinate.join("/"));
209
+ properties.coordinate = {
210
+ type: "string",
211
+ description: "Item coordinate",
212
+ enum: coordStrings
213
+ };
214
+ required.push("coordinate");
215
+ }
216
+ if (isFillable) {
217
+ if (checkable) {
218
+ properties.value = {
219
+ type: "string",
220
+ description: "Checked state",
221
+ enum: ["true", "false"]
222
+ };
223
+ } else {
224
+ const selectOptions = isSelect ? getSelectOptions(sample.element) : void 0;
225
+ properties.value = {
226
+ type: "string",
227
+ description: isSelect ? "Value to select" : "Value to set",
228
+ ...selectOptions ? { enum: selectOptions } : {}
229
+ };
230
+ }
231
+ required.push("value");
232
+ }
233
+ return withLogging({
234
+ name: toolName,
235
+ description,
236
+ inputSchema: { type: "object", properties, required },
237
+ execute: (params) => {
238
+ const coord = isForEach ? params.coordinate.split("/") : [group.refName];
239
+ try {
240
+ if (isFillable) {
241
+ const instance = automation.getInteraction(coord);
242
+ if (!instance)
243
+ return errorResult(`Element not found: ${coord.join("/")}`);
244
+ setElementValue(instance.element, params.value);
245
+ for (const evt of getValueEventTypes(instance.events)) {
246
+ automation.triggerEvent(evt, coord);
247
+ }
248
+ } else {
249
+ automation.triggerEvent("click", coord);
250
+ }
251
+ return jsonResult("Done", automation.getPageState().viewState);
252
+ } catch (e) {
253
+ return errorResult(e.message);
254
+ }
255
+ }
256
+ });
257
+ }
258
+ function setupWebMCP(automation) {
259
+ if (!navigator.modelContext) {
260
+ console.warn("[WebMCP] navigator.modelContext not available — skipping registration");
261
+ return () => {
262
+ };
263
+ }
264
+ const mc = navigator.modelContext;
265
+ const genericTools = [
266
+ makeGetPageStateTool(automation),
267
+ makeListInteractionsTool(automation),
268
+ makeTriggerInteractionTool(automation),
269
+ makeFillInputTool(automation)
270
+ ];
271
+ for (const tool of genericTools) {
272
+ mc.registerTool(tool);
273
+ }
274
+ let semanticTools = buildAndRegisterSemanticTools(mc, automation);
275
+ let lastKey = interactionKey(automation);
276
+ const unsubscribe = automation.onStateChange(() => {
277
+ const newKey = interactionKey(automation);
278
+ if (newKey !== lastKey) {
279
+ semanticTools.forEach((t) => mc.unregisterTool(t.name));
280
+ semanticTools = buildAndRegisterSemanticTools(mc, automation);
281
+ lastKey = newKey;
282
+ }
283
+ });
284
+ const allTools = () => [...genericTools, ...semanticTools];
285
+ window.webmcp = {
286
+ tools() {
287
+ const tools = allTools();
288
+ console.table(tools.map((t) => ({ name: t.name, description: t.description })));
289
+ return tools;
290
+ }
291
+ };
292
+ console.log(
293
+ `[WebMCP] Registered ${genericTools.length + semanticTools.length} tools — type webmcp.tools() to list`
294
+ );
295
+ return () => {
296
+ unsubscribe();
297
+ genericTools.forEach((t) => mc.unregisterTool(t.name));
298
+ semanticTools.forEach((t) => mc.unregisterTool(t.name));
299
+ delete window.webmcp;
300
+ };
301
+ }
302
+ function buildAndRegisterSemanticTools(mc, automation) {
303
+ const tools = buildSemanticTools(automation);
304
+ for (const tool of tools) {
305
+ mc.registerTool(tool);
306
+ }
307
+ return tools;
308
+ }
309
+ function interactionKey(automation) {
310
+ return automation.getPageState().interactions.map(
311
+ (g) => `${g.refName}:${g.items.map((i) => i.coordinate.join("/")).join(",")}`
312
+ ).join("|");
313
+ }
314
+ export {
315
+ init,
316
+ setupWebMCP
317
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@jay-framework/webmcp-plugin",
3
+ "version": "0.12.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "description": "WebMCP plugin for jay-stack — automatically exposes page interactions as WebMCP tools, resources, and prompts",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": "./dist/index.js",
11
+ "./client": "./dist/index.client.js",
12
+ "./plugin.yaml": "./plugin.yaml"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "plugin.yaml"
17
+ ],
18
+ "scripts": {
19
+ "build": "npm run build:client && npm run build:server && npm run build:types",
20
+ "build:watch": "npm run build:client -- --watch & npm run build:server -- --watch & npm run build:types -- --watch",
21
+ "build:client": "vite build",
22
+ "build:server": "vite build --ssr",
23
+ "build:types": "tsup lib/index.ts --dts-only --format esm",
24
+ "build:check-types": "tsc",
25
+ "clean": "rimraf dist",
26
+ "confirm": "npm run clean && npm run build && npm run build:check-types && npm run test",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest"
29
+ },
30
+ "dependencies": {
31
+ "@jay-framework/fullstack-component": "^0.12.0",
32
+ "@jay-framework/runtime-automation": "^0.12.0"
33
+ },
34
+ "devDependencies": {
35
+ "@jay-framework/compiler-jay-stack": "^0.12.0",
36
+ "@jay-framework/dev-environment": "^0.12.0",
37
+ "@types/node": "^20.11.5",
38
+ "rimraf": "^5.0.5",
39
+ "tsup": "^8.0.1",
40
+ "typescript": "^5.3.3",
41
+ "vite": "^5.0.11",
42
+ "vitest": "^1.2.1"
43
+ }
44
+ }
package/plugin.yaml ADDED
@@ -0,0 +1,2 @@
1
+ name: webmcp
2
+ global: true