@grampro/kit 2.7.2 → 2.7.3
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/.gbs/skills/gbs-components/SKILL.md +233 -209
- package/.gbs/skills/gbs-components/references/data-grid.md +9 -0
- package/.gbs/skills/gbs-components/references/forms.md +12 -0
- package/.gbs/skills/gbs-components/references/install.md +14 -3
- package/CHANGELOG.md +963 -861
- package/README.md +11 -3
- package/index.cjs +108 -123
- package/package.json +1 -1
- package/source/components/accordion/passport.json +1 -1
- package/source/components/alert/passport.json +1 -1
- package/source/components/avatar/passport.json +1 -1
- package/source/components/badge/passport.json +1 -1
- package/source/components/breadcrumb/passport.json +1 -1
- package/source/components/button/passport.json +1 -1
- package/source/components/card/passport.json +1 -1
- package/source/components/checkbox/passport.json +2 -2
- package/source/components/checkbox/passport.manual.json +1 -1
- package/source/components/checkbox/react/Checkbox.tsx +14 -0
- package/source/components/combobox/passport.json +3 -2
- package/source/components/combobox/passport.manual.json +1 -1
- package/source/components/combobox/react/MultiSelect.tsx +13 -0
- package/source/components/combobox/react/Select.tsx +13 -0
- package/source/components/combobox/react/surface.ts +70 -0
- package/source/components/data-grid/passport.json +6 -6
- package/source/components/data-grid/react/DataGrid.tsx +353 -351
- package/source/components/date-picker/passport.json +3 -2
- package/source/components/date-picker/passport.manual.json +1 -1
- package/source/components/date-picker/react/DatePicker.tsx +16 -2
- package/source/components/date-picker/react/DateRangePicker.tsx +16 -2
- package/source/components/date-picker/react/surface.ts +68 -0
- package/source/components/dialog/passport.json +1 -1
- package/source/components/file-uploader/passport.json +1 -1
- package/source/components/input/passport.json +2 -2
- package/source/components/input/passport.manual.json +1 -1
- package/source/components/input/react/Input.tsx +21 -0
- package/source/components/menu/passport.json +1 -1
- package/source/components/modal/passport.json +1 -1
- package/source/components/number-input/passport.json +2 -2
- package/source/components/number-input/passport.manual.json +1 -1
- package/source/components/number-input/react/NumberInput.tsx +30 -0
- package/source/components/popover/passport.json +1 -1
- package/source/components/progress/passport.json +1 -1
- package/source/components/radio-group/passport.json +2 -2
- package/source/components/radio-group/passport.manual.json +1 -1
- package/source/components/radio-group/react/RadioGroup.tsx +33 -0
- package/source/components/shared/core/agent/field-executors.ts +173 -137
- package/source/components/shared/core/agent/index.ts +21 -0
- package/source/components/shared/core/agent/surface.ts +437 -0
- package/source/components/shared/core/agent/webmcp.ts +332 -328
- package/source/components/shared/core/agent/write.ts +62 -58
- package/source/components/shared/index.ts +7 -0
- package/source/components/shared/react/AgentSurface.tsx +145 -0
- package/source/components/shared/react/agentSurfaceContext.ts +92 -0
- package/source/components/shared/react/useFieldAgent.ts +62 -88
- package/source/components/shared/version.json +4 -4
- package/source/components/shared/version.ts +6 -6
- package/source/components/skeleton/passport.json +1 -1
- package/source/components/spinner/passport.json +1 -1
- package/source/components/switch/passport.json +2 -2
- package/source/components/switch/passport.manual.json +1 -1
- package/source/components/switch/react/Switch.tsx +16 -0
- package/source/components/tabs/passport.json +1 -1
- package/source/components/textarea/passport.json +2 -2
- package/source/components/textarea/passport.manual.json +1 -1
- package/source/components/textarea/react/Textarea.tsx +16 -0
- package/source/components/toaster/passport.json +1 -1
- package/source/components/tooltip/passport.json +1 -1
- package/tools/doctor/baseline.cjs +5 -4
- package/tools/mcp/server.cjs +13 -1
- package/tools/passport/extract.cjs +20 -65
- package/tools/passport/hash.cjs +18 -1
- package/tools/passport/index.cjs +4 -2
- package/tools/passport/typescript.cjs +220 -0
- package/tools/prompt.cjs +205 -0
- package/tools/ui.cjs +67 -0
|
@@ -1,328 +1,332 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* WebMCP projection: exposing an already-validated command boundary to
|
|
3
|
-
* whatever agent is driving the browser.
|
|
4
|
-
*
|
|
5
|
-
* This is an adapter and nothing else. It owns no schema, no semantics and no
|
|
6
|
-
* execution path. The chain stays:
|
|
7
|
-
*
|
|
8
|
-
* component source → passport → runtime contract → intent schema
|
|
9
|
-
* → [this file] → the component's validator → its executors → the component
|
|
10
|
-
*
|
|
11
|
-
* Nothing here knows about a grid. A component's agent satisfies
|
|
12
|
-
* `ProjectableAgent` structurally, and everything component-specific arrives
|
|
13
|
-
* through `describe` and `summarise`.
|
|
14
|
-
*
|
|
15
|
-
* Why one tool and not one per operation: the operation list is not the
|
|
16
|
-
* agent's interface — the generated intent schema is, because that is where
|
|
17
|
-
* the contract and the validation layers already meet. Twenty-one operations
|
|
18
|
-
* exposed separately would be twenty-one schemas to keep in step with a
|
|
19
|
-
* contract that already describes itself.
|
|
20
|
-
*
|
|
21
|
-
* Surface notes, each of which cost a wrong run to learn:
|
|
22
|
-
*
|
|
23
|
-
* - It is `document.modelContext`. `navigator.modelContext` was the early
|
|
24
|
-
* spelling and is deprecated since Chromium 150.
|
|
25
|
-
* - Chrome hands `inputSchema` to the agent as a JSON *string*, and returns
|
|
26
|
-
* a tool result as a JSON string. That is the browser's wire format, not
|
|
27
|
-
* something to pre-serialise here.
|
|
28
|
-
* - It needs `--enable-features=WebMCPTesting`, the
|
|
29
|
-
* `chrome://flags/#enable-webmcp-testing` flag, or an origin-trial token.
|
|
30
|
-
*
|
|
31
|
-
* The specification is a Draft Community Group Report and is not on the W3C
|
|
32
|
-
* standards track, so this module is deliberately small and replaceable.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import type { ConfirmRequest, JsonSchema, ValidationIssue, ValidationLayer } from "./types";
|
|
36
|
-
|
|
37
|
-
// ----------------------------------------------------------- browser surface
|
|
38
|
-
|
|
39
|
-
/** One command the validator accepted, in the form this projection reports. */
|
|
40
|
-
export interface ProjectedCommand {
|
|
41
|
-
readonly intent: unknown;
|
|
42
|
-
readonly explain: { readonly summary: string };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* What the validator answers. Mirrors the component-level execution result;
|
|
47
|
-
* a component may carry extra fields, which are ignored here.
|
|
48
|
-
*/
|
|
49
|
-
export type ProjectedOutcome =
|
|
50
|
-
| { status: "done"; commands: readonly ProjectedCommand[]; warnings: readonly ValidationIssue[] }
|
|
51
|
-
| {
|
|
52
|
-
status: "needs-confirmation";
|
|
53
|
-
commands: readonly ProjectedCommand[];
|
|
54
|
-
warnings: readonly ValidationIssue[];
|
|
55
|
-
confirm: ConfirmRequest;
|
|
56
|
-
}
|
|
57
|
-
| {
|
|
58
|
-
status: "rejected";
|
|
59
|
-
reason: string;
|
|
60
|
-
code: string;
|
|
61
|
-
layer: ValidationLayer;
|
|
62
|
-
suggestion?: string;
|
|
63
|
-
}
|
|
64
|
-
| { status: "clarify"; question: string; options?: readonly string[] }
|
|
65
|
-
| { status: "declined"; reason: string };
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The part of a component's agent this projection uses.
|
|
69
|
-
*
|
|
70
|
-
* Structural on purpose: a DataGrid agent, a DatePicker agent or a test double
|
|
71
|
-
* satisfies it without importing anything from here.
|
|
72
|
-
*/
|
|
73
|
-
export interface ProjectableAgent {
|
|
74
|
-
/** The generated per-instance schema for one intent. */
|
|
75
|
-
schema(): JsonSchema;
|
|
76
|
-
/** Validate without changing anything. */
|
|
77
|
-
validate(intent: unknown): ProjectedOutcome;
|
|
78
|
-
execute(intent: unknown, options?: { confirm?: boolean }): Promise<ProjectedOutcome>;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** The slice of the WebMCP surface this projection touches. */
|
|
82
|
-
export interface ModelContext {
|
|
83
|
-
registerTool(descriptor: {
|
|
84
|
-
name: string;
|
|
85
|
-
description: string;
|
|
86
|
-
inputSchema: unknown;
|
|
87
|
-
execute(input: unknown): Promise<unknown>;
|
|
88
|
-
}): void | Promise<void>;
|
|
89
|
-
getTools?(): unknown;
|
|
90
|
-
executeTool?(tool: unknown, input: unknown): Promise<unknown>;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
declare global {
|
|
94
|
-
interface Document {
|
|
95
|
-
modelContext?: ModelContext;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// -------------------------------------------------------------------- result
|
|
100
|
-
|
|
101
|
-
/** What the agent gets back, before the MCP content envelope is put round it. */
|
|
102
|
-
export interface ToolResultPayload extends Record<string, unknown> {
|
|
103
|
-
ok: boolean;
|
|
104
|
-
status: string;
|
|
105
|
-
message: string;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface ToolCallLog {
|
|
109
|
-
readonly input: unknown;
|
|
110
|
-
readonly result: ToolResultPayload;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface RegistrationResult {
|
|
114
|
-
registered: boolean;
|
|
115
|
-
toolName: string;
|
|
116
|
-
/** Why not, when `registered` is false. Safe to show a developer. */
|
|
117
|
-
reason?: string;
|
|
118
|
-
/** The schema handed to the agent, for inspection in a test or a UI. */
|
|
119
|
-
inputSchema?: JsonSchema;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export interface RegisterAgentToolOptions<TAgent extends ProjectableAgent> {
|
|
123
|
-
/** The tool name the agent sees. One per page. */
|
|
124
|
-
name: string;
|
|
125
|
-
/** Natural-language description, built from the live contract. */
|
|
126
|
-
describe: (agent: TAgent) => string;
|
|
127
|
-
/** Extra fields merged into a successful result — row counts, undo state. */
|
|
128
|
-
summarise?: (agent: TAgent) => Record<string, unknown>;
|
|
129
|
-
/** How many operations may be sent in one call. Default 8. */
|
|
130
|
-
maxIntents?: number;
|
|
131
|
-
/** Description for the `intents` array itself. */
|
|
132
|
-
intentsDescription?: string;
|
|
133
|
-
/** Observe every call, for a UI log or a test. Must not throw. */
|
|
134
|
-
onCall?: (entry: ToolCallLog) => void;
|
|
135
|
-
/**
|
|
136
|
-
* Where to register. Defaults to `document.modelContext`. Supplying one is
|
|
137
|
-
* for tests and for hosts that polyfill the surface elsewhere.
|
|
138
|
-
*/
|
|
139
|
-
modelContext?: ModelContext;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const UNAVAILABLE =
|
|
143
|
-
"document.modelContext is unavailable. Chrome needs --enable-features=WebMCPTesting, " +
|
|
144
|
-
"the chrome://flags/#enable-webmcp-testing flag, or an origin-trial token.";
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Whether this page can register WebMCP tools at all.
|
|
148
|
-
*
|
|
149
|
-
* Guards `document` rather than assuming it: this module is imported by code
|
|
150
|
-
* that also runs during server rendering, where touching `document` throws.
|
|
151
|
-
*/
|
|
152
|
-
export function webmcpAvailable(modelContext?: ModelContext): boolean {
|
|
153
|
-
const target = modelContext ?? (typeof document === "undefined" ? undefined : document.modelContext);
|
|
154
|
-
return typeof target?.registerTool === "function";
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* The tool's input schema, derived from the live contract.
|
|
159
|
-
*
|
|
160
|
-
* Not hand-written. `agent.schema()` is the generated per-instance schema, so
|
|
161
|
-
* a tool registered against a component with different columns, operators or
|
|
162
|
-
* policy advertises different arguments automatically.
|
|
163
|
-
*/
|
|
164
|
-
export function buildToolInputSchema(
|
|
165
|
-
agent: ProjectableAgent,
|
|
166
|
-
options: { maxIntents?: number; intentsDescription?: string } = {},
|
|
167
|
-
): JsonSchema {
|
|
168
|
-
return {
|
|
169
|
-
type: "object",
|
|
170
|
-
additionalProperties: false,
|
|
171
|
-
required: ["intents"],
|
|
172
|
-
properties: {
|
|
173
|
-
intents: {
|
|
174
|
-
type: "array",
|
|
175
|
-
minItems: 1,
|
|
176
|
-
maxItems: options.maxIntents ?? 8,
|
|
177
|
-
description:
|
|
178
|
-
options.intentsDescription ??
|
|
179
|
-
"Operations to apply in order. Several filters on different fields combine with AND.",
|
|
180
|
-
items: agent.schema(),
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* the same
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* WebMCP projection: exposing an already-validated command boundary to
|
|
3
|
+
* whatever agent is driving the browser.
|
|
4
|
+
*
|
|
5
|
+
* This is an adapter and nothing else. It owns no schema, no semantics and no
|
|
6
|
+
* execution path. The chain stays:
|
|
7
|
+
*
|
|
8
|
+
* component source → passport → runtime contract → intent schema
|
|
9
|
+
* → [this file] → the component's validator → its executors → the component
|
|
10
|
+
*
|
|
11
|
+
* Nothing here knows about a grid. A component's agent satisfies
|
|
12
|
+
* `ProjectableAgent` structurally, and everything component-specific arrives
|
|
13
|
+
* through `describe` and `summarise`.
|
|
14
|
+
*
|
|
15
|
+
* Why one tool and not one per operation: the operation list is not the
|
|
16
|
+
* agent's interface — the generated intent schema is, because that is where
|
|
17
|
+
* the contract and the validation layers already meet. Twenty-one operations
|
|
18
|
+
* exposed separately would be twenty-one schemas to keep in step with a
|
|
19
|
+
* contract that already describes itself.
|
|
20
|
+
*
|
|
21
|
+
* Surface notes, each of which cost a wrong run to learn:
|
|
22
|
+
*
|
|
23
|
+
* - It is `document.modelContext`. `navigator.modelContext` was the early
|
|
24
|
+
* spelling and is deprecated since Chromium 150.
|
|
25
|
+
* - Chrome hands `inputSchema` to the agent as a JSON *string*, and returns
|
|
26
|
+
* a tool result as a JSON string. That is the browser's wire format, not
|
|
27
|
+
* something to pre-serialise here.
|
|
28
|
+
* - It needs `--enable-features=WebMCPTesting`, the
|
|
29
|
+
* `chrome://flags/#enable-webmcp-testing` flag, or an origin-trial token.
|
|
30
|
+
*
|
|
31
|
+
* The specification is a Draft Community Group Report and is not on the W3C
|
|
32
|
+
* standards track, so this module is deliberately small and replaceable.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { ConfirmRequest, JsonSchema, ValidationIssue, ValidationLayer } from "./types";
|
|
36
|
+
|
|
37
|
+
// ----------------------------------------------------------- browser surface
|
|
38
|
+
|
|
39
|
+
/** One command the validator accepted, in the form this projection reports. */
|
|
40
|
+
export interface ProjectedCommand {
|
|
41
|
+
readonly intent: unknown;
|
|
42
|
+
readonly explain: { readonly summary: string };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What the validator answers. Mirrors the component-level execution result;
|
|
47
|
+
* a component may carry extra fields, which are ignored here.
|
|
48
|
+
*/
|
|
49
|
+
export type ProjectedOutcome =
|
|
50
|
+
| { status: "done"; commands: readonly ProjectedCommand[]; warnings: readonly ValidationIssue[] }
|
|
51
|
+
| {
|
|
52
|
+
status: "needs-confirmation";
|
|
53
|
+
commands: readonly ProjectedCommand[];
|
|
54
|
+
warnings: readonly ValidationIssue[];
|
|
55
|
+
confirm: ConfirmRequest;
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
status: "rejected";
|
|
59
|
+
reason: string;
|
|
60
|
+
code: string;
|
|
61
|
+
layer: ValidationLayer;
|
|
62
|
+
suggestion?: string;
|
|
63
|
+
}
|
|
64
|
+
| { status: "clarify"; question: string; options?: readonly string[] }
|
|
65
|
+
| { status: "declined"; reason: string };
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The part of a component's agent this projection uses.
|
|
69
|
+
*
|
|
70
|
+
* Structural on purpose: a DataGrid agent, a DatePicker agent or a test double
|
|
71
|
+
* satisfies it without importing anything from here.
|
|
72
|
+
*/
|
|
73
|
+
export interface ProjectableAgent {
|
|
74
|
+
/** The generated per-instance schema for one intent. */
|
|
75
|
+
schema(): JsonSchema;
|
|
76
|
+
/** Validate without changing anything. */
|
|
77
|
+
validate(intent: unknown): ProjectedOutcome;
|
|
78
|
+
execute(intent: unknown, options?: { confirm?: boolean }): Promise<ProjectedOutcome>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The slice of the WebMCP surface this projection touches. */
|
|
82
|
+
export interface ModelContext {
|
|
83
|
+
registerTool(descriptor: {
|
|
84
|
+
name: string;
|
|
85
|
+
description: string;
|
|
86
|
+
inputSchema: unknown;
|
|
87
|
+
execute(input: unknown): Promise<unknown>;
|
|
88
|
+
}): void | Promise<void>;
|
|
89
|
+
getTools?(): unknown;
|
|
90
|
+
executeTool?(tool: unknown, input: unknown): Promise<unknown>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
declare global {
|
|
94
|
+
interface Document {
|
|
95
|
+
modelContext?: ModelContext;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// -------------------------------------------------------------------- result
|
|
100
|
+
|
|
101
|
+
/** What the agent gets back, before the MCP content envelope is put round it. */
|
|
102
|
+
export interface ToolResultPayload extends Record<string, unknown> {
|
|
103
|
+
ok: boolean;
|
|
104
|
+
status: string;
|
|
105
|
+
message: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ToolCallLog {
|
|
109
|
+
readonly input: unknown;
|
|
110
|
+
readonly result: ToolResultPayload;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface RegistrationResult {
|
|
114
|
+
registered: boolean;
|
|
115
|
+
toolName: string;
|
|
116
|
+
/** Why not, when `registered` is false. Safe to show a developer. */
|
|
117
|
+
reason?: string;
|
|
118
|
+
/** The schema handed to the agent, for inspection in a test or a UI. */
|
|
119
|
+
inputSchema?: JsonSchema;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface RegisterAgentToolOptions<TAgent extends ProjectableAgent> {
|
|
123
|
+
/** The tool name the agent sees. One per page. */
|
|
124
|
+
name: string;
|
|
125
|
+
/** Natural-language description, built from the live contract. */
|
|
126
|
+
describe: (agent: TAgent) => string;
|
|
127
|
+
/** Extra fields merged into a successful result — row counts, undo state. */
|
|
128
|
+
summarise?: (agent: TAgent) => Record<string, unknown>;
|
|
129
|
+
/** How many operations may be sent in one call. Default 8. */
|
|
130
|
+
maxIntents?: number;
|
|
131
|
+
/** Description for the `intents` array itself. */
|
|
132
|
+
intentsDescription?: string;
|
|
133
|
+
/** Observe every call, for a UI log or a test. Must not throw. */
|
|
134
|
+
onCall?: (entry: ToolCallLog) => void;
|
|
135
|
+
/**
|
|
136
|
+
* Where to register. Defaults to `document.modelContext`. Supplying one is
|
|
137
|
+
* for tests and for hosts that polyfill the surface elsewhere.
|
|
138
|
+
*/
|
|
139
|
+
modelContext?: ModelContext;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const UNAVAILABLE =
|
|
143
|
+
"document.modelContext is unavailable. Chrome needs --enable-features=WebMCPTesting, " +
|
|
144
|
+
"the chrome://flags/#enable-webmcp-testing flag, or an origin-trial token.";
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Whether this page can register WebMCP tools at all.
|
|
148
|
+
*
|
|
149
|
+
* Guards `document` rather than assuming it: this module is imported by code
|
|
150
|
+
* that also runs during server rendering, where touching `document` throws.
|
|
151
|
+
*/
|
|
152
|
+
export function webmcpAvailable(modelContext?: ModelContext): boolean {
|
|
153
|
+
const target = modelContext ?? (typeof document === "undefined" ? undefined : document.modelContext);
|
|
154
|
+
return typeof target?.registerTool === "function";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The tool's input schema, derived from the live contract.
|
|
159
|
+
*
|
|
160
|
+
* Not hand-written. `agent.schema()` is the generated per-instance schema, so
|
|
161
|
+
* a tool registered against a component with different columns, operators or
|
|
162
|
+
* policy advertises different arguments automatically.
|
|
163
|
+
*/
|
|
164
|
+
export function buildToolInputSchema(
|
|
165
|
+
agent: ProjectableAgent,
|
|
166
|
+
options: { maxIntents?: number; intentsDescription?: string } = {},
|
|
167
|
+
): JsonSchema {
|
|
168
|
+
return {
|
|
169
|
+
type: "object",
|
|
170
|
+
additionalProperties: false,
|
|
171
|
+
required: ["intents"],
|
|
172
|
+
properties: {
|
|
173
|
+
intents: {
|
|
174
|
+
type: "array",
|
|
175
|
+
minItems: 1,
|
|
176
|
+
maxItems: options.maxIntents ?? 8,
|
|
177
|
+
description:
|
|
178
|
+
options.intentsDescription ??
|
|
179
|
+
"Operations to apply in order. Several filters on different fields combine with AND.",
|
|
180
|
+
items: agent.schema(),
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The production pipeline for one batch of intents, independent of how the
|
|
188
|
+
* request arrived.
|
|
189
|
+
*
|
|
190
|
+
* validate → a refusal is returned as the validator wrote it
|
|
191
|
+
* confirm → anything irreversible stops here and asks, rather than running
|
|
192
|
+
* execute → only after validation passed
|
|
193
|
+
*
|
|
194
|
+
* There is no repair, no retry, and no path that exists only for one caller.
|
|
195
|
+
* A WebMCP tool for one component, the page-wide AgentSurface tool, and an
|
|
196
|
+
* app's own assistant all go through this, so an agent sending something a
|
|
197
|
+
* component cannot do gets the same refusal, with the same code and layer,
|
|
198
|
+
* whichever door it used — and the same refusal a form would have got.
|
|
199
|
+
*/
|
|
200
|
+
export async function runProjected(
|
|
201
|
+
agent: ProjectableAgent,
|
|
202
|
+
intents: unknown,
|
|
203
|
+
options: { summarise?: () => Record<string, unknown> } = {},
|
|
204
|
+
): Promise<ToolResultPayload> {
|
|
205
|
+
if (!Array.isArray(intents) || intents.length === 0) {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
status: "rejected",
|
|
209
|
+
code: "missing-intents",
|
|
210
|
+
message: '"intents" must be a non-empty array of operations.',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* Layer one of the real pipeline. Nothing has touched the component yet. */
|
|
215
|
+
let checked: ProjectedOutcome;
|
|
216
|
+
try {
|
|
217
|
+
checked = agent.validate(intents);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
status: "rejected",
|
|
222
|
+
code: "validator-threw",
|
|
223
|
+
message: error instanceof Error ? error.message : String(error),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (checked.status === "rejected") {
|
|
228
|
+
return {
|
|
229
|
+
ok: false,
|
|
230
|
+
status: "rejected",
|
|
231
|
+
code: checked.code,
|
|
232
|
+
layer: checked.layer,
|
|
233
|
+
suggestion: checked.suggestion,
|
|
234
|
+
message: checked.reason,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (checked.status === "clarify") {
|
|
238
|
+
return { ok: false, status: "clarify", message: checked.question, options: checked.options };
|
|
239
|
+
}
|
|
240
|
+
if (checked.status === "declined") {
|
|
241
|
+
return { ok: false, status: "declined", message: checked.reason };
|
|
242
|
+
}
|
|
243
|
+
if (checked.status === "needs-confirmation") {
|
|
244
|
+
/*
|
|
245
|
+
* An agent does not get to skip a confirmation a person would see. The
|
|
246
|
+
* component is left untouched and the request is handed back.
|
|
247
|
+
*/
|
|
248
|
+
return {
|
|
249
|
+
ok: false,
|
|
250
|
+
status: "needs-confirmation",
|
|
251
|
+
code: checked.confirm.code,
|
|
252
|
+
message: checked.confirm.message,
|
|
253
|
+
explain: checked.commands.map((command) => command.explain.summary),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let run: ProjectedOutcome;
|
|
258
|
+
try {
|
|
259
|
+
run = await agent.execute(intents);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return {
|
|
262
|
+
ok: false,
|
|
263
|
+
status: "failed",
|
|
264
|
+
code: "executor-threw",
|
|
265
|
+
message: error instanceof Error ? error.message : String(error),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (run.status !== "done") {
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
status: run.status,
|
|
273
|
+
message: "reason" in run ? run.reason : "question" in run ? run.question : run.status,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
status: "done",
|
|
280
|
+
message: run.commands.map((command) => command.explain.summary).join("; "),
|
|
281
|
+
applied: run.commands.map((command) => command.intent),
|
|
282
|
+
warnings: run.warnings.map((warning) => `${warning.code}: ${warning.message}`),
|
|
283
|
+
...(options.summarise?.() ?? {}),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** A payload in MCP's tool-result shape: text for a reader, structure for code. */
|
|
288
|
+
export function toToolResult(payload: ToolResultPayload) {
|
|
289
|
+
return {
|
|
290
|
+
content: [{ type: "text", text: payload.message }],
|
|
291
|
+
structuredContent: payload,
|
|
292
|
+
isError: payload.ok === false,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Registers one tool for one component's agent, if the browser has WebMCP.
|
|
298
|
+
* Every call runs `runProjected`; see there for what that guarantees.
|
|
299
|
+
*/
|
|
300
|
+
export function registerAgentTool<TAgent extends ProjectableAgent>(
|
|
301
|
+
agent: TAgent,
|
|
302
|
+
options: RegisterAgentToolOptions<TAgent>,
|
|
303
|
+
): RegistrationResult {
|
|
304
|
+
const modelContext =
|
|
305
|
+
options.modelContext ?? (typeof document === "undefined" ? undefined : document.modelContext);
|
|
306
|
+
|
|
307
|
+
if (typeof modelContext?.registerTool !== "function") {
|
|
308
|
+
return { registered: false, toolName: options.name, reason: UNAVAILABLE };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const inputSchema = buildToolInputSchema(agent, options);
|
|
312
|
+
|
|
313
|
+
modelContext.registerTool({
|
|
314
|
+
name: options.name,
|
|
315
|
+
description: options.describe(agent),
|
|
316
|
+
inputSchema,
|
|
317
|
+
async execute(rawInput: unknown) {
|
|
318
|
+
const intents = (rawInput as { intents?: unknown } | null | undefined)?.intents;
|
|
319
|
+
const payload = await runProjected(agent, intents, {
|
|
320
|
+
summarise: options.summarise ? () => options.summarise!(agent) : undefined,
|
|
321
|
+
});
|
|
322
|
+
try {
|
|
323
|
+
options.onCall?.({ input: rawInput, result: payload });
|
|
324
|
+
} catch {
|
|
325
|
+
/* A broken observer must not turn a good command into an error. */
|
|
326
|
+
}
|
|
327
|
+
return toToolResult(payload);
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return { registered: true, toolName: options.name, inputSchema };
|
|
332
|
+
}
|