taskia-prototype-mcp 3.0.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.
- package/README.md +120 -0
- package/package.json +24 -0
- package/server.js +536 -0
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# taskia-prototype-mcp
|
|
2
|
+
|
|
3
|
+
MCP server that lets agents **browse and see** the live Taskia design prototype.
|
|
4
|
+
|
|
5
|
+
The MCP opens a real Chromium browser pointed at the published GitHub Pages URL.
|
|
6
|
+
Agents can navigate screens, click UI elements, hover for tooltips, scroll — and every
|
|
7
|
+
action returns a screenshot so they can see exactly what happened.
|
|
8
|
+
|
|
9
|
+
Because it connects to the **live GitHub Pages deployment**, agents always see
|
|
10
|
+
Andreina's latest design — no local clone, no `REPO_PATH` required.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Tools
|
|
15
|
+
|
|
16
|
+
| Tool | What it does |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `screenshot` | PNG of whatever is visible right now |
|
|
19
|
+
| `go_to_screen` | Navigate to a named screen + screenshot |
|
|
20
|
+
| `click` | Click an element by visible text + screenshot |
|
|
21
|
+
| `hover` | Hover an element (tooltips / hover states) + screenshot |
|
|
22
|
+
| `scroll` | Scroll the page + screenshot |
|
|
23
|
+
| `list_interactive_elements` | Every clickable element currently on screen |
|
|
24
|
+
| `reload` | Reload from GitHub Pages (picks up Andreina's latest push) |
|
|
25
|
+
| `reset` | Return to default screen (Tasks, signed in) |
|
|
26
|
+
| `list_screens` | All screen names available in the prototype |
|
|
27
|
+
| `get_design_tokens` | CSS custom properties (light + dark) from live `styles.css` |
|
|
28
|
+
| `get_design_system_docs` | Full `DESIGN_SYSTEM.md` |
|
|
29
|
+
| `get_prototype_url` | The live GitHub Pages URL |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
### 1. Clone the repo
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/Omicron-Group-Solutions/prototipo_diseno.git
|
|
39
|
+
cd prototipo_diseno/mcp-server
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Install dependencies + Chromium
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install
|
|
46
|
+
npm run install-browser
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> `install-browser` downloads a local Chromium (~130 MB, one-time).
|
|
50
|
+
|
|
51
|
+
### 3. Add to Claude Code
|
|
52
|
+
|
|
53
|
+
Edit `~/.claude/claude_desktop_config.json`:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"taskia-prototype": {
|
|
59
|
+
"command": "node",
|
|
60
|
+
"args": ["C:\\Users\\You\\Desktop\\prototipo_diseno\\mcp-server\\server.js"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> No `REPO_PATH` needed — the MCP fetches everything from GitHub Pages.
|
|
67
|
+
|
|
68
|
+
### 4. Restart Claude Code
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Watch the browser (optional)
|
|
73
|
+
|
|
74
|
+
Add `HEADLESS=false` to see the browser window on screen while agents drive it:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"taskia-prototype": {
|
|
80
|
+
"command": "node",
|
|
81
|
+
"args": ["C:\\Users\\You\\Desktop\\prototipo_diseno\\mcp-server\\server.js"],
|
|
82
|
+
"env": {
|
|
83
|
+
"HEADLESS": "false"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Staying up to date
|
|
93
|
+
|
|
94
|
+
When Andreina pushes a design update, call `reload()` — the browser refreshes from
|
|
95
|
+
GitHub Pages and returns a screenshot of the new design. No restart needed.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Example usage
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
"Show me the Tasks screen"
|
|
103
|
+
→ go_to_screen({ screen_name: "ScreenTasks" })
|
|
104
|
+
|
|
105
|
+
"What can I click on the Kanban board?"
|
|
106
|
+
→ go_to_screen({ screen_name: "ScreenKanban" })
|
|
107
|
+
→ list_interactive_elements()
|
|
108
|
+
|
|
109
|
+
"Click that task to open the detail panel"
|
|
110
|
+
→ click({ text: "Diseño nuevo sistema" })
|
|
111
|
+
|
|
112
|
+
"Show the hover state on the sidebar"
|
|
113
|
+
→ hover({ text: "Kanban" })
|
|
114
|
+
|
|
115
|
+
"What color is the primary button?"
|
|
116
|
+
→ get_design_tokens() ← look for --brand or --btn-*
|
|
117
|
+
|
|
118
|
+
"Andreina just pushed an update — show me"
|
|
119
|
+
→ reload()
|
|
120
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "taskia-prototype-mcp",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "MCP server — browse the live Taskia prototype, screenshot any screen, interact with UI elements",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"taskia-prototype-mcp": "server.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"server.js"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "node server.js",
|
|
14
|
+
"install-browser": "playwright install chromium",
|
|
15
|
+
"postinstall": "playwright install chromium"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
22
|
+
"playwright": "^1.49.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* taskia-prototype-mcp v3
|
|
4
|
+
*
|
|
5
|
+
* Design-focused MCP that browses the LIVE GitHub Pages prototype.
|
|
6
|
+
*
|
|
7
|
+
* No local clone required — everything is fetched from the published URL,
|
|
8
|
+
* so the MCP always reflects Andreina's latest push automatically.
|
|
9
|
+
*
|
|
10
|
+
* A persistent Playwright session keeps one browser open across all tool
|
|
11
|
+
* calls. Set HEADLESS=false in the env to show the browser window on
|
|
12
|
+
* screen while agents drive it.
|
|
13
|
+
*
|
|
14
|
+
* Tools
|
|
15
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
16
|
+
* screenshot — PNG of whatever is visible right now
|
|
17
|
+
* go_to_screen — jump to a named screen
|
|
18
|
+
* click — click an element by visible text
|
|
19
|
+
* hover — hover an element (tooltips / hover states)
|
|
20
|
+
* scroll — scroll the page in any direction
|
|
21
|
+
* list_interactive_elements — every clickable element on screen
|
|
22
|
+
* reload — reload from GitHub Pages (picks up latest push)
|
|
23
|
+
* reset — return to default screen (Tasks, signed in)
|
|
24
|
+
* list_screens — all screen names in the prototype
|
|
25
|
+
* get_design_tokens — CSS custom properties (light + dark)
|
|
26
|
+
* get_design_system_docs — full DESIGN_SYSTEM.md
|
|
27
|
+
* get_prototype_url — the live GitHub Pages URL
|
|
28
|
+
*
|
|
29
|
+
* Optional env vars:
|
|
30
|
+
* HEADLESS=false → shows the browser window (watch agents drive the prototype)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
34
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
35
|
+
import {
|
|
36
|
+
CallToolRequestSchema,
|
|
37
|
+
ListToolsRequestSchema,
|
|
38
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
39
|
+
import { chromium } from 'playwright';
|
|
40
|
+
|
|
41
|
+
// ── Config ─────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const PAGES_URL = 'https://omicron-group-solutions.github.io/prototipo_diseno/';
|
|
44
|
+
const HEADLESS = process.env.HEADLESS !== 'false'; // default: headless; set HEADLESS=false to show window
|
|
45
|
+
|
|
46
|
+
const SCREEN_IDS = {
|
|
47
|
+
ScreenLogin: 'login',
|
|
48
|
+
ScreenDashboard: 'dashboard',
|
|
49
|
+
ScreenTasks: 'tasks',
|
|
50
|
+
ScreenKanban: 'kanban',
|
|
51
|
+
ScreenCalendar: 'calendar',
|
|
52
|
+
ScreenReports: 'reports',
|
|
53
|
+
ScreenContacts: 'contacts',
|
|
54
|
+
ScreenLabels: 'labels',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ── Persistent browser session ─────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
let _browser = null;
|
|
60
|
+
let _page = null;
|
|
61
|
+
|
|
62
|
+
/** Returns the live page, launching a new browser if needed. */
|
|
63
|
+
async function getPage() {
|
|
64
|
+
if (_page) {
|
|
65
|
+
try {
|
|
66
|
+
await _page.title(); // ping — throws if page is dead
|
|
67
|
+
return _page;
|
|
68
|
+
} catch {
|
|
69
|
+
_page = null;
|
|
70
|
+
_browser = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_browser = await chromium.launch({
|
|
75
|
+
headless: HEADLESS,
|
|
76
|
+
args: HEADLESS ? [] : ['--start-maximized'],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const ctx = await _browser.newContext({
|
|
80
|
+
viewport: { width: 1440, height: 900 },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
_page = await ctx.newPage();
|
|
84
|
+
await _page.goto(PAGES_URL, { waitUntil: 'networkidle', timeout: 30_000 });
|
|
85
|
+
await _page.waitForSelector('.app', { timeout: 20_000 });
|
|
86
|
+
|
|
87
|
+
return _page;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Clean up when the MCP server exits
|
|
91
|
+
process.on('SIGTERM', () => _browser?.close().catch(() => {}));
|
|
92
|
+
process.on('SIGINT', () => _browser?.close().catch(() => {}));
|
|
93
|
+
|
|
94
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
async function snap() {
|
|
97
|
+
const page = await getPage();
|
|
98
|
+
const png = await page.screenshot({ type: 'png', fullPage: false });
|
|
99
|
+
return png.toString('base64');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Fetch a file from the live GitHub Pages deployment. */
|
|
103
|
+
async function fetchAsset(path) {
|
|
104
|
+
const url = PAGES_URL.replace(/\/$/, '') + '/' + path.replace(/^\//, '');
|
|
105
|
+
const res = await fetch(url);
|
|
106
|
+
if (!res.ok)
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Could not fetch "${path}" from GitHub Pages (HTTP ${res.status}). ` +
|
|
109
|
+
`Make sure GitHub Pages is published at ${PAGES_URL}`
|
|
110
|
+
);
|
|
111
|
+
return res.text();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseDesignTokens(css) {
|
|
115
|
+
const light = {}, dark = {};
|
|
116
|
+
for (const [, block] of css.matchAll(/:root(?:\[.*?\])?\s*\{([^}]+)\}/g))
|
|
117
|
+
for (const [, k, v] of block.matchAll(/--([^:]+):\s*([^;]+);/g))
|
|
118
|
+
light[`--${k.trim()}`] = v.trim();
|
|
119
|
+
for (const [, block] of css.matchAll(/\[data-theme="dark"\]\s*\{([^}]+)\}/g))
|
|
120
|
+
for (const [, k, v] of block.matchAll(/--([^:]+):\s*([^;]+);/g))
|
|
121
|
+
dark[`--${k.trim()}`] = v.trim();
|
|
122
|
+
return { light, dark };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── MCP server ─────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
const server = new Server(
|
|
128
|
+
{ name: 'taskia-prototype', version: '3.0.0' },
|
|
129
|
+
{ capabilities: { tools: {} } }
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
133
|
+
tools: [
|
|
134
|
+
{
|
|
135
|
+
name: 'screenshot',
|
|
136
|
+
description:
|
|
137
|
+
'Returns a PNG of whatever the prototype browser is showing right now. ' +
|
|
138
|
+
'Call after any navigation or interaction to see the result.',
|
|
139
|
+
inputSchema: { type: 'object', properties: {} },
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'go_to_screen',
|
|
143
|
+
description:
|
|
144
|
+
'Navigate the prototype to a specific screen and return a screenshot. ' +
|
|
145
|
+
'Call list_screens to get valid names.',
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: 'object',
|
|
148
|
+
required: ['screen_name'],
|
|
149
|
+
properties: {
|
|
150
|
+
screen_name: {
|
|
151
|
+
type: 'string',
|
|
152
|
+
description:
|
|
153
|
+
'Screen component name, e.g. ScreenTasks, ScreenKanban, ScreenLogin, ' +
|
|
154
|
+
'ScreenDashboard, ScreenCalendar, ScreenReports, ScreenContacts, ScreenLabels.',
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'click',
|
|
161
|
+
description:
|
|
162
|
+
'Click an element by its visible text. ' +
|
|
163
|
+
'Call list_interactive_elements to see what is on screen. ' +
|
|
164
|
+
'Returns a screenshot of the state after the click.',
|
|
165
|
+
inputSchema: {
|
|
166
|
+
type: 'object',
|
|
167
|
+
required: ['text'],
|
|
168
|
+
properties: {
|
|
169
|
+
text: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
description: 'Visible label / text of the element to click (partial match, case-insensitive).',
|
|
172
|
+
},
|
|
173
|
+
exact: {
|
|
174
|
+
type: 'boolean',
|
|
175
|
+
description: 'Match the text exactly. Default false (partial match).',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'hover',
|
|
182
|
+
description:
|
|
183
|
+
'Hover over an element to reveal tooltips, hover states, or dropdowns. ' +
|
|
184
|
+
'Returns a screenshot of the hover state.',
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: 'object',
|
|
187
|
+
required: ['text'],
|
|
188
|
+
properties: {
|
|
189
|
+
text: {
|
|
190
|
+
type: 'string',
|
|
191
|
+
description: 'Visible text of the element to hover.',
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'scroll',
|
|
198
|
+
description: 'Scroll the prototype. Returns a screenshot of the new position.',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
required: ['direction'],
|
|
202
|
+
properties: {
|
|
203
|
+
direction: {
|
|
204
|
+
type: 'string',
|
|
205
|
+
enum: ['up', 'down', 'left', 'right'],
|
|
206
|
+
},
|
|
207
|
+
amount: {
|
|
208
|
+
type: 'number',
|
|
209
|
+
description: 'Pixels to scroll. Default 300.',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'list_interactive_elements',
|
|
216
|
+
description:
|
|
217
|
+
'Lists every visible, interactive element on the current screen — ' +
|
|
218
|
+
'buttons, nav items, tabs, links, inputs. ' +
|
|
219
|
+
'Use this before calling click() to discover what is available.',
|
|
220
|
+
inputSchema: { type: 'object', properties: {} },
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'reload',
|
|
224
|
+
description:
|
|
225
|
+
'Reload the prototype from GitHub Pages to pick up the latest design Andreina has pushed. ' +
|
|
226
|
+
'Returns a screenshot of the freshly loaded prototype.',
|
|
227
|
+
inputSchema: { type: 'object', properties: {} },
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'reset',
|
|
231
|
+
description: 'Return to the default screen (Tasks, signed in). Start fresh from a known state.',
|
|
232
|
+
inputSchema: { type: 'object', properties: {} },
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'list_screens',
|
|
236
|
+
description: 'Lists all screens in the prototype. Use these names with go_to_screen.',
|
|
237
|
+
inputSchema: { type: 'object', properties: {} },
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'get_design_tokens',
|
|
241
|
+
description:
|
|
242
|
+
'Returns all CSS custom properties (colors, spacing, radius, blur) for both themes, ' +
|
|
243
|
+
'fetched live from the published prototype. Source of truth for design values.',
|
|
244
|
+
inputSchema: { type: 'object', properties: {} },
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: 'get_design_system_docs',
|
|
248
|
+
description:
|
|
249
|
+
'Returns the full DESIGN_SYSTEM.md — every token, component pattern, and design decision. ' +
|
|
250
|
+
'Read before implementing any screen.',
|
|
251
|
+
inputSchema: { type: 'object', properties: {} },
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'get_prototype_url',
|
|
255
|
+
description: 'Returns the live GitHub Pages URL of the Taskia prototype.',
|
|
256
|
+
inputSchema: { type: 'object', properties: {} },
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
}));
|
|
260
|
+
|
|
261
|
+
// ── Tool handlers ──────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
264
|
+
const { name, arguments: args = {} } = request.params;
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
switch (name) {
|
|
268
|
+
|
|
269
|
+
// ── screenshot ──────────────────────────────────────────────────────
|
|
270
|
+
case 'screenshot': {
|
|
271
|
+
const png = await snap();
|
|
272
|
+
return {
|
|
273
|
+
content: [
|
|
274
|
+
{ type: 'text', text: 'Current prototype state:' },
|
|
275
|
+
{ type: 'image', data: png, mimeType: 'image/png' },
|
|
276
|
+
],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── go_to_screen ────────────────────────────────────────────────────
|
|
281
|
+
case 'go_to_screen': {
|
|
282
|
+
const page = await getPage();
|
|
283
|
+
const screenName = args.screen_name ?? '';
|
|
284
|
+
const id = SCREEN_IDS[screenName] ?? screenName.toLowerCase();
|
|
285
|
+
|
|
286
|
+
if (id === 'login') {
|
|
287
|
+
await page.evaluate(() => window.__setSignedIn?.(false));
|
|
288
|
+
} else {
|
|
289
|
+
await page.evaluate((sid) => {
|
|
290
|
+
window.__setSignedIn?.(true);
|
|
291
|
+
window.__setScreen?.(sid);
|
|
292
|
+
}, id);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
await page.waitForTimeout(600);
|
|
296
|
+
|
|
297
|
+
const png = await snap();
|
|
298
|
+
return {
|
|
299
|
+
content: [
|
|
300
|
+
{ type: 'text', text: `Navigated to ${screenName} (id: "${id}")` },
|
|
301
|
+
{ type: 'image', data: png, mimeType: 'image/png' },
|
|
302
|
+
],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── click ───────────────────────────────────────────────────────────
|
|
307
|
+
case 'click': {
|
|
308
|
+
const page = await getPage();
|
|
309
|
+
const exact = args.exact ?? false;
|
|
310
|
+
const locator = page.getByText(args.text, { exact });
|
|
311
|
+
const count = await locator.count();
|
|
312
|
+
|
|
313
|
+
if (count === 0) {
|
|
314
|
+
return {
|
|
315
|
+
content: [{ type: 'text', text: `No visible element found with text "${args.text}". Call list_interactive_elements to see what is on screen.` }],
|
|
316
|
+
isError: true,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
await locator.first().click({ timeout: 5_000 });
|
|
321
|
+
await page.waitForTimeout(400);
|
|
322
|
+
|
|
323
|
+
const png = await snap();
|
|
324
|
+
return {
|
|
325
|
+
content: [
|
|
326
|
+
{ type: 'text', text: `Clicked "${args.text}"` },
|
|
327
|
+
{ type: 'image', data: png, mimeType: 'image/png' },
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── hover ───────────────────────────────────────────────────────────
|
|
333
|
+
case 'hover': {
|
|
334
|
+
const page = await getPage();
|
|
335
|
+
const locator = page.getByText(args.text, { exact: false });
|
|
336
|
+
const count = await locator.count();
|
|
337
|
+
|
|
338
|
+
if (count === 0) {
|
|
339
|
+
return {
|
|
340
|
+
content: [{ type: 'text', text: `No visible element found with text "${args.text}"` }],
|
|
341
|
+
isError: true,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await locator.first().hover({ timeout: 5_000 });
|
|
346
|
+
await page.waitForTimeout(350);
|
|
347
|
+
|
|
348
|
+
const png = await snap();
|
|
349
|
+
return {
|
|
350
|
+
content: [
|
|
351
|
+
{ type: 'text', text: `Hovering over "${args.text}"` },
|
|
352
|
+
{ type: 'image', data: png, mimeType: 'image/png' },
|
|
353
|
+
],
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ── scroll ──────────────────────────────────────────────────────────
|
|
358
|
+
case 'scroll': {
|
|
359
|
+
const page = await getPage();
|
|
360
|
+
const amount = args.amount ?? 300;
|
|
361
|
+
const deltas = {
|
|
362
|
+
down: [0, amount],
|
|
363
|
+
up: [0, -amount],
|
|
364
|
+
right: [ amount, 0],
|
|
365
|
+
left: [-amount, 0],
|
|
366
|
+
};
|
|
367
|
+
const [dx, dy] = deltas[args.direction] ?? [0, 300];
|
|
368
|
+
await page.mouse.wheel(dx, dy);
|
|
369
|
+
await page.waitForTimeout(300);
|
|
370
|
+
|
|
371
|
+
const png = await snap();
|
|
372
|
+
return {
|
|
373
|
+
content: [
|
|
374
|
+
{ type: 'text', text: `Scrolled ${args.direction} ${amount}px` },
|
|
375
|
+
{ type: 'image', data: png, mimeType: 'image/png' },
|
|
376
|
+
],
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── list_interactive_elements ────────────────────────────────────────
|
|
381
|
+
case 'list_interactive_elements': {
|
|
382
|
+
const page = await getPage();
|
|
383
|
+
|
|
384
|
+
const elements = await page.evaluate(() => {
|
|
385
|
+
const SELECTORS = [
|
|
386
|
+
'button', 'a[href]',
|
|
387
|
+
'[role="button"]', '[role="link"]', '[role="tab"]',
|
|
388
|
+
'[role="menuitem"]', '[role="option"]',
|
|
389
|
+
'input', 'select', '[onclick]',
|
|
390
|
+
'.nav-item', '.ws-item', '.sidebar-item', '.tab', '.btn',
|
|
391
|
+
];
|
|
392
|
+
|
|
393
|
+
const seen = new Set();
|
|
394
|
+
const results = [];
|
|
395
|
+
|
|
396
|
+
for (const sel of SELECTORS) {
|
|
397
|
+
for (const el of document.querySelectorAll(sel)) {
|
|
398
|
+
const rect = el.getBoundingClientRect();
|
|
399
|
+
// skip invisible or off-screen elements
|
|
400
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
401
|
+
if (rect.top > window.innerHeight + 20 || rect.bottom < -20) continue;
|
|
402
|
+
|
|
403
|
+
const text = (
|
|
404
|
+
el.textContent ||
|
|
405
|
+
el.getAttribute('aria-label') ||
|
|
406
|
+
el.getAttribute('title') ||
|
|
407
|
+
el.getAttribute('placeholder') ||
|
|
408
|
+
''
|
|
409
|
+
).trim().replace(/\s+/g, ' ').slice(0, 60);
|
|
410
|
+
|
|
411
|
+
if (!text) continue;
|
|
412
|
+
|
|
413
|
+
const key = `${el.tagName}|${text}`;
|
|
414
|
+
if (seen.has(key)) continue;
|
|
415
|
+
seen.add(key);
|
|
416
|
+
|
|
417
|
+
const role = el.getAttribute('role');
|
|
418
|
+
const tag = el.tagName.toLowerCase() + (role ? `[role=${role}]` : '');
|
|
419
|
+
results.push({ tag, text });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return results;
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const lines = elements.map(e => ` [${e.tag}] "${e.text}"`).join('\n');
|
|
427
|
+
return {
|
|
428
|
+
content: [{
|
|
429
|
+
type: 'text',
|
|
430
|
+
text: `Interactive elements (${elements.length}):\n${lines}\n\nUse click({ text: "..." }) with any of these labels.`,
|
|
431
|
+
}],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── reload ──────────────────────────────────────────────────────────
|
|
436
|
+
case 'reload': {
|
|
437
|
+
const page = await getPage();
|
|
438
|
+
await page.reload({ waitUntil: 'networkidle', timeout: 30_000 });
|
|
439
|
+
await page.waitForSelector('.app', { timeout: 20_000 });
|
|
440
|
+
await page.waitForTimeout(500);
|
|
441
|
+
|
|
442
|
+
const png = await snap();
|
|
443
|
+
return {
|
|
444
|
+
content: [
|
|
445
|
+
{ type: 'text', text: `Prototype reloaded — now showing the latest design from:\n${PAGES_URL}` },
|
|
446
|
+
{ type: 'image', data: png, mimeType: 'image/png' },
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ── reset ───────────────────────────────────────────────────────────
|
|
452
|
+
case 'reset': {
|
|
453
|
+
const page = await getPage();
|
|
454
|
+
await page.evaluate(() => {
|
|
455
|
+
window.__setSignedIn?.(true);
|
|
456
|
+
window.__setScreen?.('tasks');
|
|
457
|
+
});
|
|
458
|
+
await page.waitForTimeout(600);
|
|
459
|
+
|
|
460
|
+
const png = await snap();
|
|
461
|
+
return {
|
|
462
|
+
content: [
|
|
463
|
+
{ type: 'text', text: 'Reset to default screen (Tasks, signed in).' },
|
|
464
|
+
{ type: 'image', data: png, mimeType: 'image/png' },
|
|
465
|
+
],
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ── list_screens ─────────────────────────────────────────────────────
|
|
470
|
+
case 'list_screens': {
|
|
471
|
+
const source = await fetchAsset('screens.jsx');
|
|
472
|
+
const screens = [...source.matchAll(/^function\s+(Screen[A-Za-z]+)\s*\(/gm)].map(m => m[1]);
|
|
473
|
+
return {
|
|
474
|
+
content: [{
|
|
475
|
+
type: 'text',
|
|
476
|
+
text: `Screens (${screens.length}):\n${screens.map(s => ` • ${s}`).join('\n')}\n\nUse go_to_screen with any of these names.`,
|
|
477
|
+
}],
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── get_design_tokens ─────────────────────────────────────────────────
|
|
482
|
+
case 'get_design_tokens': {
|
|
483
|
+
const css = await fetchAsset('styles.css');
|
|
484
|
+
const { light, dark } = parseDesignTokens(css);
|
|
485
|
+
const fmt = obj => Object.entries(obj).map(([k, v]) => ` ${k}: ${v}`).join('\n');
|
|
486
|
+
return {
|
|
487
|
+
content: [{
|
|
488
|
+
type: 'text',
|
|
489
|
+
text: [
|
|
490
|
+
`### Light theme (${Object.keys(light).length} tokens)`,
|
|
491
|
+
fmt(light),
|
|
492
|
+
'',
|
|
493
|
+
`### Dark theme overrides (${Object.keys(dark).length})`,
|
|
494
|
+
fmt(dark),
|
|
495
|
+
].join('\n'),
|
|
496
|
+
}],
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ── get_design_system_docs ────────────────────────────────────────────
|
|
501
|
+
case 'get_design_system_docs': {
|
|
502
|
+
const docs = await fetchAsset('DESIGN_SYSTEM.md');
|
|
503
|
+
return { content: [{ type: 'text', text: docs }] };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ── get_prototype_url ─────────────────────────────────────────────────
|
|
507
|
+
case 'get_prototype_url':
|
|
508
|
+
return {
|
|
509
|
+
content: [{
|
|
510
|
+
type: 'text',
|
|
511
|
+
text: [
|
|
512
|
+
`Live prototype : ${PAGES_URL}`,
|
|
513
|
+
`Source repo : https://github.com/Omicron-Group-Solutions/prototipo_diseno`,
|
|
514
|
+
'',
|
|
515
|
+
'Always reflects the latest design Andreina has pushed to main.',
|
|
516
|
+
'Call reload() after a push to refresh the browser session.',
|
|
517
|
+
].join('\n'),
|
|
518
|
+
}],
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
default:
|
|
522
|
+
throw new Error(`Unknown tool: "${name}"`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
} catch (err) {
|
|
526
|
+
return {
|
|
527
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
528
|
+
isError: true,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// ── Start ──────────────────────────────────────────────────────────────────
|
|
534
|
+
|
|
535
|
+
const transport = new StdioServerTransport();
|
|
536
|
+
await server.connect(transport);
|