@ultimat3/mcp 20.1.3 → 20.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/mcp",
3
- "version": "20.1.3",
3
+ "version": "20.1.5",
4
4
  "description": "MCP server, dev tools, and the action-to-tool projection \u2014 one authz system, two surfaces",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,13 +31,13 @@
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@ultimat3/action": "20.1.3",
35
- "@ultimat3/core": "20.1.3",
36
- "@ultimat3/entity": "20.1.3",
37
- "@ultimat3/http": "20.1.3",
38
- "@ultimat3/jobs": "20.1.3",
39
- "@ultimat3/policy": "20.1.3",
40
- "@ultimat3/query": "20.1.3",
41
- "@ultimat3/schema": "20.1.3"
34
+ "@ultimat3/action": "20.1.5",
35
+ "@ultimat3/core": "20.1.5",
36
+ "@ultimat3/entity": "20.1.5",
37
+ "@ultimat3/http": "20.1.5",
38
+ "@ultimat3/jobs": "20.1.5",
39
+ "@ultimat3/policy": "20.1.5",
40
+ "@ultimat3/query": "20.1.5",
41
+ "@ultimat3/schema": "20.1.5"
42
42
  }
43
43
  }
package/src/dev-server.ts CHANGED
@@ -68,6 +68,59 @@ export interface VerifyResult {
68
68
  }
69
69
 
70
70
  /** Description sources. Satisfied by `frameworkIntrospection` in a real app. */
71
+ /**
72
+ * The viewports `ui.shot` names. Named, not free, so two agents (or one agent twice) photograph
73
+ * the same thing and can compare the pictures; `{ width, height }` stays available for the one
74
+ * case a name does not cover.
75
+ */
76
+ export const UI_VIEWPORTS = {
77
+ phone: { width: 390, height: 844 },
78
+ tablet: { width: 820, height: 1180 },
79
+ desktop: { width: 1440, height: 900 },
80
+ } as const;
81
+ export type UiViewportName = keyof typeof UI_VIEWPORTS;
82
+ export type UiColorScheme = 'light' | 'dark';
83
+
84
+ export interface UiShotInput {
85
+ /** The route's path — `/dashboard`, `/links/abc123` — never a full URL. */
86
+ readonly route: string;
87
+ readonly viewport: { readonly width: number; readonly height: number };
88
+ /**
89
+ * What `prefers-color-scheme` the page sees. Emulated on the page BEFORE navigation, so a
90
+ * capture never depends on the box that took it; an app whose boot script honours a stored
91
+ * choice still wins, because the stored choice is what "explicit" means.
92
+ */
93
+ readonly colorScheme: UiColorScheme;
94
+ readonly fullPage: boolean;
95
+ }
96
+
97
+ /**
98
+ * What a picture is worth: the file, and the verdict beside it. The verdict is the SAME shape
99
+ * `x shot` writes to `verdict.json` — console lines, page errors, network refusals, whether every
100
+ * island mounted — so a picture with a hydration error is a finding, never merely a picture.
101
+ * The PNG is a PATH, never inlined bytes: an agent reads the picture it wants and pays for one.
102
+ */
103
+ export interface UiShotResult {
104
+ readonly ok: boolean;
105
+ readonly image: string;
106
+ readonly verdictFile: string;
107
+ readonly verdict: unknown;
108
+ }
109
+
110
+ export interface UiIslandInput {
111
+ /** The island's name as `x shot --island <name>` takes it. */
112
+ readonly island: string;
113
+ /** One declared state, or every state the island declares. */
114
+ readonly state?: string | undefined;
115
+ }
116
+
117
+ export interface UiIslandResult {
118
+ readonly ok: boolean;
119
+ readonly dir: string;
120
+ readonly verdictFile: string;
121
+ readonly verdict: unknown;
122
+ }
123
+
71
124
  export interface DevIntrospection {
72
125
  routes(): unknown;
73
126
  entities(): unknown;
@@ -94,6 +147,15 @@ export interface DevCapabilities {
94
147
  readManifest(): Promise<string>;
95
148
  explainError(code: string): ErrorExplanation | undefined;
96
149
  verify(fix: boolean): Promise<VerifyResult>;
150
+ /**
151
+ * Photograph one route against the running dev server (or a scratch one), the way `x shot
152
+ * <route>` does, at a viewport and colour scheme the caller names. Refuses a route that declares
153
+ * no JS budget: a picture of a route nobody has finished is a picture of a draft, and the gate
154
+ * refuses the same route as `X_BUDGET_UNMEASURED`.
155
+ */
156
+ shotRoute(input: UiShotInput): Promise<UiShotResult>;
157
+ /** `x shot --island <name> [--state <id>]` as a tool: every declared state, photographed and judged. */
158
+ shotIsland(input: UiIslandInput): Promise<UiIslandResult>;
97
159
  }
98
160
 
99
161
  export type DevHost = DevIntrospection & DevCapabilities;
@@ -104,6 +166,19 @@ const NAME_ARG: JsonSchema = {
104
166
  additionalProperties: false,
105
167
  };
106
168
 
169
+ /**
170
+ * An explicit `width`+`height` wins over the name; one of the pair alone is not a viewport and
171
+ * falls back to the name (default `desktop`) rather than to a half-sized frame.
172
+ */
173
+ export function viewportOf(args: ToolArgs): { readonly width: number; readonly height: number } {
174
+ const width = args['width'];
175
+ const height = args['height'];
176
+ if (typeof width === 'number' && typeof height === 'number') return { width, height };
177
+ const name = args['viewport'];
178
+ const named = typeof name === 'string' && Object.hasOwn(UI_VIEWPORTS, name) ? name : 'desktop';
179
+ return UI_VIEWPORTS[named as UiViewportName];
180
+ }
181
+
107
182
  /** Every dev tool, in one array so `x mcp serve` and the HTTP transport share the catalog. */
108
183
  export function devTools(host: DevHost): readonly AnyMcpTool[] {
109
184
  return [
@@ -258,6 +333,67 @@ export function devTools(host: DevHost): readonly AnyMcpTool[] {
258
333
  return { ...jsonResult(result), ...(result.ok ? {} : { isError: true }) };
259
334
  },
260
335
  },
336
+ {
337
+ name: 'ui.shot',
338
+ description:
339
+ 'Photograph one route at a named viewport (phone/tablet/desktop) or an explicit size, ' +
340
+ 'in light or dark, against the running dev server. Returns the PNG path and the same ' +
341
+ 'verdict x shot writes: console, page errors, refused requests, whether every island ' +
342
+ 'mounted. Refuses a route with no declared JS budget. Launches a browser.',
343
+ scope: DEV_SCOPES.test,
344
+ destructive: true,
345
+ inputSchema: {
346
+ type: 'object',
347
+ properties: {
348
+ route: { type: 'string', description: 'Route path, e.g. /dashboard.' },
349
+ viewport: {
350
+ type: 'string',
351
+ enum: Object.keys(UI_VIEWPORTS),
352
+ default: 'desktop',
353
+ description: 'phone 390×844, tablet 820×1180, desktop 1440×900.',
354
+ },
355
+ width: { type: 'integer', minimum: 320, maximum: 3840 },
356
+ height: { type: 'integer', minimum: 320, maximum: 2160 },
357
+ theme: { type: 'string', enum: ['light', 'dark'], default: 'dark' },
358
+ fullPage: { type: 'boolean', default: true },
359
+ },
360
+ required: ['route'],
361
+ additionalProperties: false,
362
+ },
363
+ async handle(args: ToolArgs) {
364
+ const route = typeof args['route'] === 'string' ? args['route'] : '';
365
+ const result = await host.shotRoute({
366
+ route,
367
+ viewport: viewportOf(args),
368
+ colorScheme: args['theme'] === 'light' ? 'light' : 'dark',
369
+ fullPage: args['fullPage'] !== false,
370
+ });
371
+ return { ...jsonResult(result), ...(result.ok ? {} : { isError: true }) };
372
+ },
373
+ },
374
+ {
375
+ name: 'ui.island',
376
+ description:
377
+ 'Photograph an island in every state its *.island.states.ts declares (or one state), ' +
378
+ 'as x shot --island does: PNGs plus a verdict per state. Launches a browser.',
379
+ scope: DEV_SCOPES.test,
380
+ destructive: true,
381
+ inputSchema: {
382
+ type: 'object',
383
+ properties: {
384
+ island: { type: 'string', description: 'Island name, e.g. links-table.' },
385
+ state: { type: 'string', description: 'One declared state id; omit for all.' },
386
+ },
387
+ required: ['island'],
388
+ additionalProperties: false,
389
+ },
390
+ async handle(args: ToolArgs) {
391
+ const island = typeof args['island'] === 'string' ? args['island'] : '';
392
+ const state = typeof args['state'] === 'string' ? args['state'] : undefined;
393
+ const result = await host.shotIsland({ island, ...(state === undefined ? {} : { state }) });
394
+ return { ...jsonResult(result), ...(result.ok ? {} : { isError: true }) };
395
+ },
396
+ },
261
397
  {
262
398
  name: 'logs.tail',
263
399
  description: 'Last N log lines, optionally for one runtime role (web/sync/worker/...).',
package/src/index.ts CHANGED
@@ -25,10 +25,16 @@ export type {
25
25
  MigrateResult,
26
26
  QueueDepth,
27
27
  TestRun,
28
+ UiColorScheme,
29
+ UiIslandInput,
30
+ UiIslandResult,
31
+ UiShotInput,
32
+ UiShotResult,
33
+ UiViewportName,
28
34
  VerifyResult,
29
35
  VerifyStep,
30
36
  } from './dev-server';
31
- export { DEV_SCOPES, devTools } from './dev-server';
37
+ export { DEV_SCOPES, devTools, UI_VIEWPORTS, viewportOf } from './dev-server';
32
38
  export type { McpErrorCode } from './errors';
33
39
  export {
34
40
  MCP_ERROR_CODES,