taskia-prototype-mcp 3.0.0 → 3.1.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 +129 -120
- package/package.json +24 -24
- package/server.js +439 -7
package/README.md
CHANGED
|
@@ -1,120 +1,129 @@
|
|
|
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
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
| `get_html` | Cleaned, pretty-printed **rendered HTML** of a screen or element — near copy-paste ready |
|
|
25
|
+
| `get_css` | **Authored + computed CSS** for an element (matched rules with `var()` names, plus resolved px/color values) |
|
|
26
|
+
| `reload` | Reload from GitHub Pages (picks up Andreina's latest push) |
|
|
27
|
+
| `reset` | Return to default screen (Tasks, signed in) |
|
|
28
|
+
| `list_screens` | All screen names available in the prototype |
|
|
29
|
+
| `get_design_tokens` | CSS custom properties (light + dark) from live `styles.css` |
|
|
30
|
+
| `get_design_system_docs` | Full `DESIGN_SYSTEM.md` |
|
|
31
|
+
| `get_prototype_url` | The live GitHub Pages URL |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Setup
|
|
36
|
+
|
|
37
|
+
### 1. Clone the repo
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/Omicron-Group-Solutions/prototipo_diseno.git
|
|
41
|
+
cd prototipo_diseno/mcp-server
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2. Install dependencies + Chromium
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install
|
|
48
|
+
npm run install-browser
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> `install-browser` downloads a local Chromium (~130 MB, one-time).
|
|
52
|
+
|
|
53
|
+
### 3. Add to Claude Code
|
|
54
|
+
|
|
55
|
+
Edit `~/.claude/claude_desktop_config.json`:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"mcpServers": {
|
|
60
|
+
"taskia-prototype": {
|
|
61
|
+
"command": "node",
|
|
62
|
+
"args": ["C:\\Users\\You\\Desktop\\prototipo_diseno\\mcp-server\\server.js"]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> No `REPO_PATH` needed — the MCP fetches everything from GitHub Pages.
|
|
69
|
+
|
|
70
|
+
### 4. Restart Claude Code
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Watch the browser (optional)
|
|
75
|
+
|
|
76
|
+
Add `HEADLESS=false` to see the browser window on screen while agents drive it:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"taskia-prototype": {
|
|
82
|
+
"command": "node",
|
|
83
|
+
"args": ["C:\\Users\\You\\Desktop\\prototipo_diseno\\mcp-server\\server.js"],
|
|
84
|
+
"env": {
|
|
85
|
+
"HEADLESS": "false"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Staying up to date
|
|
95
|
+
|
|
96
|
+
When Andreina pushes a design update, call `reload()` — the browser refreshes from
|
|
97
|
+
GitHub Pages and returns a screenshot of the new design. No restart needed.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Example usage
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
"Show me the Tasks screen"
|
|
105
|
+
→ go_to_screen({ screen_name: "ScreenTasks" })
|
|
106
|
+
|
|
107
|
+
"What can I click on the Kanban board?"
|
|
108
|
+
→ go_to_screen({ screen_name: "ScreenKanban" })
|
|
109
|
+
→ list_interactive_elements()
|
|
110
|
+
|
|
111
|
+
"Click that task to open the detail panel"
|
|
112
|
+
→ click({ text: "Diseño nuevo sistema" })
|
|
113
|
+
|
|
114
|
+
"Show the hover state on the sidebar"
|
|
115
|
+
→ hover({ text: "Kanban" })
|
|
116
|
+
|
|
117
|
+
"What color is the primary button?"
|
|
118
|
+
→ get_design_tokens() ← look for --brand or --btn-*
|
|
119
|
+
|
|
120
|
+
"Give me the markup for this card so I can rebuild it"
|
|
121
|
+
→ get_html({ selector: ".doc-card" }) ← or { text: "Factura" }
|
|
122
|
+
|
|
123
|
+
"What CSS makes the sidebar look like that?"
|
|
124
|
+
→ get_css({ selector: ".sidebar" }) ← matched rules + resolved values
|
|
125
|
+
→ get_css({ selector: ".sidebar", all: true }) ← full computed style dump
|
|
126
|
+
|
|
127
|
+
"Andreina just pushed an update — show me"
|
|
128
|
+
→ reload()
|
|
129
|
+
```
|
package/package.json
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "taskia-prototype-mcp",
|
|
3
|
-
"version": "3.
|
|
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
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "taskia-prototype-mcp",
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"description": "MCP server — browse the live Taskia prototype, screenshot any screen, interact with UI elements, extract rendered HTML/CSS",
|
|
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
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* hover — hover an element (tooltips / hover states)
|
|
20
20
|
* scroll — scroll the page in any direction
|
|
21
21
|
* list_interactive_elements — every clickable element on screen
|
|
22
|
+
* get_html — cleaned, pretty-printed rendered HTML of a screen/element
|
|
23
|
+
* get_css — authored + resolved computed CSS for an element
|
|
22
24
|
* reload — reload from GitHub Pages (picks up latest push)
|
|
23
25
|
* reset — return to default screen (Tasks, signed in)
|
|
24
26
|
* list_screens — all screen names in the prototype
|
|
@@ -54,6 +56,96 @@ const SCREEN_IDS = {
|
|
|
54
56
|
ScreenLabels: 'labels',
|
|
55
57
|
};
|
|
56
58
|
|
|
59
|
+
// Pointer CRM — its own React SPA under /crm/ (separate page from the Taskia
|
|
60
|
+
// SPA). Reached by loading crm/index.html once, then driving it with the same
|
|
61
|
+
// window.__setScreen(id) hook Taskia uses. window.__APP === 'crm' marks it.
|
|
62
|
+
const CRM_APP_URL = 'crm/index.html';
|
|
63
|
+
const CRM_SCREENS = {
|
|
64
|
+
CrmContacts: 'contacts', // listado personas/empresas — entry point
|
|
65
|
+
CrmContact: 'contact', // ficha de contacto
|
|
66
|
+
CrmCompany: 'company', // ficha de empresa
|
|
67
|
+
CrmNewContact: 'new-contact', // alta de contacto
|
|
68
|
+
CrmDeals: 'deals', // pipeline (kanban + lista)
|
|
69
|
+
CrmDeal: 'deal', // ficha de oportunidad
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Pointer ERP — its own React SPA under /pointer/ (the full 6-module ERP).
|
|
73
|
+
// Same mechanics as the CRM SPA: load pointer/index.html once, then drive it
|
|
74
|
+
// with window.__setScreen(id). window.__APP === 'pointer' marks it. Screen ids
|
|
75
|
+
// mirror the source app route hashes (e.g. `currency`, `purchase-request`).
|
|
76
|
+
const POINTER_APP_URL = 'pointer/index.html';
|
|
77
|
+
const POINTER_SCREENS = {
|
|
78
|
+
PointerHome: 'home',
|
|
79
|
+
PointerContactView: 'contact-view',
|
|
80
|
+
PointerContactGroups: 'contact-groups',
|
|
81
|
+
PointerPaymentCondition: 'payment-condition',
|
|
82
|
+
PointerCurrency: 'currency',
|
|
83
|
+
PointerAdvancesReceived: 'advances-received',
|
|
84
|
+
PointerAdvancesGiven: 'advances-given',
|
|
85
|
+
PointerIncome: 'income',
|
|
86
|
+
PointerExpenses: 'expenses',
|
|
87
|
+
PointerAccountPayable: 'account-payable',
|
|
88
|
+
PointerAccountReceivable: 'account-receivable',
|
|
89
|
+
PointerCollection: 'collection',
|
|
90
|
+
PointerPayment: 'payment',
|
|
91
|
+
PointerCurrencyExchange: 'currency-exchange',
|
|
92
|
+
PointerFundTransfer: 'fund-transfer',
|
|
93
|
+
PointerCashRegister: 'cash-register',
|
|
94
|
+
PointerBankAccount: 'bank-account',
|
|
95
|
+
PointerSerie: 'serie',
|
|
96
|
+
PointerAdministrationConfiguration: 'administration-configuration',
|
|
97
|
+
PointerLocation: 'location',
|
|
98
|
+
PointerPriceList: 'price-list',
|
|
99
|
+
PointerItem: 'item',
|
|
100
|
+
PointerUnit: 'unit',
|
|
101
|
+
PointerVariable: 'variable',
|
|
102
|
+
PointerDeliveryNote: 'delivery-note',
|
|
103
|
+
PointerReceiptNote: 'receipt-note',
|
|
104
|
+
PointerConfigurationDescription: 'configuration-description',
|
|
105
|
+
PointerOrderPreparationRequest: 'order-preparation-request',
|
|
106
|
+
PointerOrderPreparation: 'order-preparation',
|
|
107
|
+
PointerInventoryCheckPlanning: 'inventory-check-planning',
|
|
108
|
+
PointerInventoryCheck: 'inventory-check',
|
|
109
|
+
PointerInventoryTransfer: 'inventory-transfer',
|
|
110
|
+
PointerInventoryAdjustment: 'inventory-adjustment',
|
|
111
|
+
PointerProductionOrder: 'production-order',
|
|
112
|
+
PointerCategoryItem: 'category-item',
|
|
113
|
+
PointerBrand: 'brand',
|
|
114
|
+
PointerWarehouseConfiguration: 'warehouse-configuration',
|
|
115
|
+
PointerPurchaseRequest: 'purchase-request',
|
|
116
|
+
PointerPurchaseOrder: 'purchase-order',
|
|
117
|
+
PointerPurchaseInvoice: 'purchase-invoice',
|
|
118
|
+
PointerDebitNoteSuppliers: 'debit-note-suppliers',
|
|
119
|
+
PointerCreditNoteSuppliers: 'credit-note-suppliers',
|
|
120
|
+
PointerPurchaseConfiguration: 'purchase-configuration',
|
|
121
|
+
PointerAccountingAccount: 'accounting-account',
|
|
122
|
+
PointerAccountingAccountGroup: 'accounting-account-group',
|
|
123
|
+
PointerManualVoucher: 'manual-voucher',
|
|
124
|
+
PointerJournalEntry: 'journal-entry',
|
|
125
|
+
PointerTaxes: 'taxes',
|
|
126
|
+
PointerBanks: 'banks',
|
|
127
|
+
PointerBankReconciliation: 'bank-reconciliation',
|
|
128
|
+
PointerAccountingConfiguration: 'accounting-configuration',
|
|
129
|
+
PointerOpportunity: 'opportunity',
|
|
130
|
+
PointerSalesQuote: 'sales-quote',
|
|
131
|
+
PointerSalesOrder: 'sales-order',
|
|
132
|
+
PointerSalesInvoice: 'sales-invoice',
|
|
133
|
+
PointerCreditNoteCustomers: 'credit-note-customers',
|
|
134
|
+
PointerDebitNoteCustomers: 'debit-note-customers',
|
|
135
|
+
PointerAgreements: 'agreements',
|
|
136
|
+
PointerProjectCost: 'project-cost',
|
|
137
|
+
PointerProjectCostReturn: 'project-cost-return',
|
|
138
|
+
PointerSalesConfiguration: 'sales-configuration',
|
|
139
|
+
PointerLabelBuilder: 'label-builder',
|
|
140
|
+
PointerZone: 'zone',
|
|
141
|
+
PointerGeneralConfiguration: 'general-configuration',
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Absolute GitHub Pages URL for a path relative to the deployment root. */
|
|
145
|
+
function pagesUrl(path) {
|
|
146
|
+
return PAGES_URL.replace(/\/$/, '') + '/' + path.replace(/^\//, '');
|
|
147
|
+
}
|
|
148
|
+
|
|
57
149
|
// ── Persistent browser session ─────────────────────────────────────────────
|
|
58
150
|
|
|
59
151
|
let _browser = null;
|
|
@@ -82,7 +174,11 @@ async function getPage() {
|
|
|
82
174
|
|
|
83
175
|
_page = await ctx.newPage();
|
|
84
176
|
await _page.goto(PAGES_URL, { waitUntil: 'networkidle', timeout: 30_000 });
|
|
85
|
-
|
|
177
|
+
// The deployment root is the Design Hub, which does not mount `.app` — the
|
|
178
|
+
// Taskia / CRM / Pointer SPAs each mount it on their own page. Wait for `.app`
|
|
179
|
+
// if this happens to be an SPA page, but don't fail on the hub: go_to_screen
|
|
180
|
+
// navigates to the right SPA and waits for `.app` there.
|
|
181
|
+
await _page.waitForSelector('.app', { timeout: 5_000 }).catch(() => {});
|
|
86
182
|
|
|
87
183
|
return _page;
|
|
88
184
|
}
|
|
@@ -111,6 +207,29 @@ async function fetchAsset(path) {
|
|
|
111
207
|
return res.text();
|
|
112
208
|
}
|
|
113
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Resolve a target element for get_html / get_css.
|
|
212
|
+
* Precedence: explicit CSS `selector` → visible `text` → the whole screen (.app).
|
|
213
|
+
* Returns a Playwright ElementHandle plus a human label for the response.
|
|
214
|
+
*/
|
|
215
|
+
async function resolveHandle(page, { selector, text } = {}) {
|
|
216
|
+
if (selector) {
|
|
217
|
+
const handle = await page.$(selector);
|
|
218
|
+
if (!handle) throw new Error(`No element matches CSS selector "${selector}".`);
|
|
219
|
+
return { handle, label: `selector "${selector}"` };
|
|
220
|
+
}
|
|
221
|
+
if (text) {
|
|
222
|
+
const loc = page.getByText(text, { exact: false }).first();
|
|
223
|
+
if ((await loc.count()) === 0)
|
|
224
|
+
throw new Error(`No element found with text "${text}". Call list_interactive_elements to see what is on screen.`);
|
|
225
|
+
const handle = await loc.elementHandle();
|
|
226
|
+
return { handle, label: `text "${text}"` };
|
|
227
|
+
}
|
|
228
|
+
const handle = (await page.$('.app')) || (await page.$('body'));
|
|
229
|
+
if (!handle) throw new Error('Nothing is loaded on the page yet.');
|
|
230
|
+
return { handle, label: '.app (whole screen)' };
|
|
231
|
+
}
|
|
232
|
+
|
|
114
233
|
function parseDesignTokens(css) {
|
|
115
234
|
const light = {}, dark = {};
|
|
116
235
|
for (const [, block] of css.matchAll(/:root(?:\[.*?\])?\s*\{([^}]+)\}/g))
|
|
@@ -125,7 +244,7 @@ function parseDesignTokens(css) {
|
|
|
125
244
|
// ── MCP server ─────────────────────────────────────────────────────────────
|
|
126
245
|
|
|
127
246
|
const server = new Server(
|
|
128
|
-
{ name: 'taskia-prototype', version: '3.
|
|
247
|
+
{ name: 'taskia-prototype', version: '3.1.0' },
|
|
129
248
|
{ capabilities: { tools: {} } }
|
|
130
249
|
);
|
|
131
250
|
|
|
@@ -150,8 +269,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
150
269
|
screen_name: {
|
|
151
270
|
type: 'string',
|
|
152
271
|
description:
|
|
153
|
-
'
|
|
154
|
-
'
|
|
272
|
+
'Taskia app screen, e.g. ScreenTasks, ScreenKanban, ScreenLogin, ScreenDashboard, ' +
|
|
273
|
+
'ScreenCalendar, ScreenReports, ScreenContacts, ScreenLabels. ' +
|
|
274
|
+
'Or a standalone Pointer CRM screen: CrmContacts, CrmContact, CrmCompany, ' +
|
|
275
|
+
'CrmNewContact, CrmDeals, CrmDeal. Call list_screens for the full list.',
|
|
155
276
|
},
|
|
156
277
|
},
|
|
157
278
|
},
|
|
@@ -219,6 +340,59 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
219
340
|
'Use this before calling click() to discover what is available.',
|
|
220
341
|
inputSchema: { type: 'object', properties: {} },
|
|
221
342
|
},
|
|
343
|
+
{
|
|
344
|
+
name: 'get_html',
|
|
345
|
+
description:
|
|
346
|
+
'Returns the RENDERED, cleaned, pretty-printed HTML of the current screen (or a specific ' +
|
|
347
|
+
'element). Since the prototype is React, this is the exact final markup — scripts/styles ' +
|
|
348
|
+
'and event-handler attributes are stripped, whitespace is collapsed, so it is close to ' +
|
|
349
|
+
'copy-paste ready when translating a screen to code. ' +
|
|
350
|
+
'With no arguments it dumps the whole screen (.app). Pass "selector" (any CSS selector) or ' +
|
|
351
|
+
'"text" (visible label) to scope to one component.',
|
|
352
|
+
inputSchema: {
|
|
353
|
+
type: 'object',
|
|
354
|
+
properties: {
|
|
355
|
+
selector: {
|
|
356
|
+
type: 'string',
|
|
357
|
+
description: 'CSS selector of the element to extract, e.g. ".doc-editor" or "#sidebar". Takes precedence over text.',
|
|
358
|
+
},
|
|
359
|
+
text: {
|
|
360
|
+
type: 'string',
|
|
361
|
+
description: 'Visible text of the element to extract (partial match). Used when no selector is given.',
|
|
362
|
+
},
|
|
363
|
+
max_depth: {
|
|
364
|
+
type: 'number',
|
|
365
|
+
description: 'Max nesting depth before children are collapsed with a comment. Default 40 (effectively the full tree). Lower it for a quick structural overview.',
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: 'get_css',
|
|
372
|
+
description:
|
|
373
|
+
'Returns the CSS for an element two ways: (1) the AUTHORED rules from the stylesheets that ' +
|
|
374
|
+
'actually match it (with var() names and media queries intact), and (2) the KEY COMPUTED ' +
|
|
375
|
+
'styles — the final resolved px/color values the browser painted. This is the accurate ' +
|
|
376
|
+
'source of truth for translating a component to code. ' +
|
|
377
|
+
'Target the element with "selector" or "text" (defaults to the whole screen).',
|
|
378
|
+
inputSchema: {
|
|
379
|
+
type: 'object',
|
|
380
|
+
properties: {
|
|
381
|
+
selector: {
|
|
382
|
+
type: 'string',
|
|
383
|
+
description: 'CSS selector of the element to inspect. Takes precedence over text.',
|
|
384
|
+
},
|
|
385
|
+
text: {
|
|
386
|
+
type: 'string',
|
|
387
|
+
description: 'Visible text of the element to inspect (partial match). Used when no selector is given.',
|
|
388
|
+
},
|
|
389
|
+
all: {
|
|
390
|
+
type: 'boolean',
|
|
391
|
+
description: 'Return the full computed style (~350 properties) instead of the curated key set. Default false.',
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
222
396
|
{
|
|
223
397
|
name: 'reload',
|
|
224
398
|
description:
|
|
@@ -281,7 +455,60 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
281
455
|
case 'go_to_screen': {
|
|
282
456
|
const page = await getPage();
|
|
283
457
|
const screenName = args.screen_name ?? '';
|
|
284
|
-
|
|
458
|
+
|
|
459
|
+
// ── Pointer CRM SPA screens ──
|
|
460
|
+
if (CRM_SCREENS[screenName]) {
|
|
461
|
+
const id = CRM_SCREENS[screenName];
|
|
462
|
+
// Load the CRM SPA if we are not already on it (Taskia or a blank tab).
|
|
463
|
+
const onCrm = await page.evaluate(() => window.__APP === 'crm' && typeof window.__setScreen === 'function');
|
|
464
|
+
if (!onCrm) {
|
|
465
|
+
await page.goto(pagesUrl(CRM_APP_URL), { waitUntil: 'networkidle', timeout: 30_000 });
|
|
466
|
+
await page.waitForSelector('.app', { timeout: 20_000 });
|
|
467
|
+
await page.waitForFunction(() => typeof window.__setScreen === 'function', { timeout: 20_000 });
|
|
468
|
+
}
|
|
469
|
+
await page.evaluate((sid) => window.__setScreen(sid), id);
|
|
470
|
+
await page.waitForTimeout(500);
|
|
471
|
+
|
|
472
|
+
const png = await snap();
|
|
473
|
+
return {
|
|
474
|
+
content: [
|
|
475
|
+
{ type: 'text', text: `Navigated to CRM screen ${screenName} (id: "${id}")` },
|
|
476
|
+
{ type: 'image', data: png, mimeType: 'image/png' },
|
|
477
|
+
],
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── Pointer ERP SPA screens ──
|
|
482
|
+
if (POINTER_SCREENS[screenName]) {
|
|
483
|
+
const id = POINTER_SCREENS[screenName];
|
|
484
|
+
const onPointer = await page.evaluate(() => window.__APP === 'pointer' && typeof window.__setScreen === 'function');
|
|
485
|
+
if (!onPointer) {
|
|
486
|
+
await page.goto(pagesUrl(POINTER_APP_URL), { waitUntil: 'networkidle', timeout: 30_000 });
|
|
487
|
+
await page.waitForSelector('.app', { timeout: 20_000 });
|
|
488
|
+
await page.waitForFunction(() => typeof window.__setScreen === 'function', { timeout: 20_000 });
|
|
489
|
+
}
|
|
490
|
+
await page.evaluate((sid) => window.__setScreen(sid), id);
|
|
491
|
+
await page.waitForTimeout(500);
|
|
492
|
+
|
|
493
|
+
const png = await snap();
|
|
494
|
+
return {
|
|
495
|
+
content: [
|
|
496
|
+
{ type: 'text', text: `Navigated to Pointer screen ${screenName} (id: "${id}")` },
|
|
497
|
+
{ type: 'image', data: png, mimeType: 'image/png' },
|
|
498
|
+
],
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ── Taskia SPA screens ──
|
|
503
|
+
const id = SCREEN_IDS[screenName] ?? screenName.toLowerCase();
|
|
504
|
+
|
|
505
|
+
// We may currently be on the CRM SPA (or a non-SPA page) — reload the
|
|
506
|
+
// Taskia shell before driving it. Distinguish by the __APP marker.
|
|
507
|
+
const onTaskia = await page.evaluate(() => typeof window.__setScreen === 'function' && window.__APP !== 'crm' && window.__APP !== 'pointer');
|
|
508
|
+
if (!onTaskia) {
|
|
509
|
+
await page.goto(PAGES_URL, { waitUntil: 'networkidle', timeout: 30_000 });
|
|
510
|
+
await page.waitForSelector('.app', { timeout: 20_000 });
|
|
511
|
+
}
|
|
285
512
|
|
|
286
513
|
if (id === 'login') {
|
|
287
514
|
await page.evaluate(() => window.__setSignedIn?.(false));
|
|
@@ -432,11 +659,201 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
432
659
|
};
|
|
433
660
|
}
|
|
434
661
|
|
|
662
|
+
// ── get_html ──────────────────────────────────────────────────────────
|
|
663
|
+
case 'get_html': {
|
|
664
|
+
const page = await getPage();
|
|
665
|
+
const { handle, label } = await resolveHandle(page, args);
|
|
666
|
+
const maxDepth = args.max_depth ?? 40;
|
|
667
|
+
|
|
668
|
+
const html = await handle.evaluate((el, maxDepth) => {
|
|
669
|
+
const VOID = new Set([
|
|
670
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
671
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
672
|
+
]);
|
|
673
|
+
|
|
674
|
+
const esc = s => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
675
|
+
const escAttr = s => s.replace(/&/g, '&').replace(/"/g, '"');
|
|
676
|
+
|
|
677
|
+
function serialize(node, depth) {
|
|
678
|
+
const pad = ' '.repeat(depth);
|
|
679
|
+
|
|
680
|
+
// text node
|
|
681
|
+
if (node.nodeType === 3) {
|
|
682
|
+
const t = node.textContent.replace(/\s+/g, ' ').trim();
|
|
683
|
+
return t ? pad + esc(t) : '';
|
|
684
|
+
}
|
|
685
|
+
if (node.nodeType !== 1) return '';
|
|
686
|
+
|
|
687
|
+
const tag = node.tagName.toLowerCase();
|
|
688
|
+
if (tag === 'script' || tag === 'style' || tag === 'noscript') return '';
|
|
689
|
+
|
|
690
|
+
// attributes — drop event handlers, keep everything else (class, style, data-*, aria-*)
|
|
691
|
+
let attrs = '';
|
|
692
|
+
for (const a of node.attributes) {
|
|
693
|
+
if (/^on/i.test(a.name)) continue;
|
|
694
|
+
attrs += a.value === '' ? ` ${a.name}` : ` ${a.name}="${escAttr(a.value)}"`;
|
|
695
|
+
}
|
|
696
|
+
const open = `<${tag}${attrs}>`;
|
|
697
|
+
if (VOID.has(tag)) return pad + open;
|
|
698
|
+
|
|
699
|
+
const kids = [...node.childNodes];
|
|
700
|
+
if (depth >= maxDepth && node.children.length) {
|
|
701
|
+
return `${pad}${open}<!-- …${node.children.length} child element(s) collapsed --></${tag}>`;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const rendered = kids.map(k => serialize(k, depth + 1)).filter(Boolean);
|
|
705
|
+
if (rendered.length === 0) return pad + open + `</${tag}>`;
|
|
706
|
+
|
|
707
|
+
// inline when the only content is a single text node
|
|
708
|
+
if (rendered.length === 1 && kids.filter(k => k.nodeType === 1).length === 0) {
|
|
709
|
+
return pad + open + rendered[0].trim() + `</${tag}>`;
|
|
710
|
+
}
|
|
711
|
+
return pad + open + '\n' + rendered.join('\n') + '\n' + pad + `</${tag}>`;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return serialize(el, 0);
|
|
715
|
+
}, maxDepth);
|
|
716
|
+
|
|
717
|
+
await handle.dispose();
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
content: [{
|
|
721
|
+
type: 'text',
|
|
722
|
+
text: `Rendered HTML for ${label}:\n\n\`\`\`html\n${html}\n\`\`\``,
|
|
723
|
+
}],
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ── get_css ───────────────────────────────────────────────────────────
|
|
728
|
+
case 'get_css': {
|
|
729
|
+
const page = await getPage();
|
|
730
|
+
const { handle, label } = await resolveHandle(page, args);
|
|
731
|
+
const wantAll = args.all ?? false;
|
|
732
|
+
|
|
733
|
+
const data = await handle.evaluate((el, wantAll) => {
|
|
734
|
+
const cs = getComputedStyle(el);
|
|
735
|
+
|
|
736
|
+
// Curated, translation-relevant properties (the noise-free default).
|
|
737
|
+
const PROPS = [
|
|
738
|
+
'display', 'position', 'top', 'right', 'bottom', 'left', 'z-index',
|
|
739
|
+
'box-sizing', 'width', 'height', 'min-width', 'max-width', 'min-height', 'max-height',
|
|
740
|
+
'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
741
|
+
'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
742
|
+
'flex-direction', 'flex-wrap', 'justify-content', 'align-items', 'align-content',
|
|
743
|
+
'align-self', 'gap', 'row-gap', 'column-gap', 'flex-grow', 'flex-shrink', 'flex-basis', 'order',
|
|
744
|
+
'grid-template-columns', 'grid-template-rows', 'grid-template-areas', 'grid-auto-flow',
|
|
745
|
+
'grid-auto-columns', 'grid-auto-rows', 'grid-column', 'grid-row',
|
|
746
|
+
'font-family', 'font-size', 'font-weight', 'font-style', 'line-height', 'letter-spacing',
|
|
747
|
+
'text-align', 'text-transform', 'text-decoration-line', 'white-space', 'text-overflow',
|
|
748
|
+
'color', 'background-color', 'background-image', 'background-size', 'background-position',
|
|
749
|
+
'border-width', 'border-style', 'border-color', 'border-radius',
|
|
750
|
+
'box-shadow', 'outline', 'opacity', 'transform', 'transition', 'filter', 'backdrop-filter',
|
|
751
|
+
'cursor', 'overflow', 'overflow-x', 'overflow-y', 'object-fit',
|
|
752
|
+
];
|
|
753
|
+
|
|
754
|
+
// Values that are just the CSS initial default — skip them in curated mode.
|
|
755
|
+
const SKIP = {
|
|
756
|
+
'transform': 'none', 'box-shadow': 'none', 'background-image': 'none',
|
|
757
|
+
'filter': 'none', 'backdrop-filter': 'none', 'opacity': '1', 'z-index': 'auto',
|
|
758
|
+
'letter-spacing': 'normal', 'text-transform': 'none', 'text-decoration-line': 'none',
|
|
759
|
+
'outline': 'rgb(0, 0, 0) none 0px', 'transition': 'all 0s ease 0s',
|
|
760
|
+
'text-overflow': 'clip', 'object-fit': 'fill', 'text-align': 'start',
|
|
761
|
+
'align-self': 'auto', 'order': '0', 'flex-grow': '0', 'flex-shrink': '1',
|
|
762
|
+
'top': 'auto', 'right': 'auto', 'bottom': 'auto', 'left': 'auto',
|
|
763
|
+
'min-width': 'auto', 'min-height': 'auto', 'max-width': 'none', 'max-height': 'none',
|
|
764
|
+
'flex-basis': 'auto', 'box-sizing': 'border-box', 'font-style': 'normal',
|
|
765
|
+
'white-space': 'normal', 'cursor': 'auto',
|
|
766
|
+
'overflow': 'visible', 'overflow-x': 'visible', 'overflow-y': 'visible',
|
|
767
|
+
'gap': 'normal', 'row-gap': 'normal', 'column-gap': 'normal',
|
|
768
|
+
'background-size': 'auto', 'background-position': '0% 0%',
|
|
769
|
+
'grid-template-columns': 'none', 'grid-template-rows': 'none',
|
|
770
|
+
'grid-template-areas': 'none', 'grid-auto-flow': 'row',
|
|
771
|
+
'grid-auto-columns': 'auto', 'grid-auto-rows': 'auto',
|
|
772
|
+
'grid-column': 'auto', 'grid-row': 'auto',
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
const computed = {};
|
|
776
|
+
const list = wantAll ? Array.from(cs).sort() : PROPS;
|
|
777
|
+
for (const p of list) {
|
|
778
|
+
const v = cs.getPropertyValue(p).trim();
|
|
779
|
+
if (!v) continue;
|
|
780
|
+
if (!wantAll && SKIP[p] === v) continue;
|
|
781
|
+
computed[p] = v;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Authored rules from the stylesheets that match this element.
|
|
785
|
+
const matched = [];
|
|
786
|
+
const testMatch = (sel) => {
|
|
787
|
+
// strip pseudo-classes/elements that el.matches() can't evaluate
|
|
788
|
+
const clean = sel.replace(/::?[a-zA-Z-]+(\([^)]*\))?/g, '').trim();
|
|
789
|
+
if (!clean) return false;
|
|
790
|
+
try { return el.matches(clean); } catch { return false; }
|
|
791
|
+
};
|
|
792
|
+
const walk = (rules, condition) => {
|
|
793
|
+
for (const rule of rules) {
|
|
794
|
+
if (rule.type === 1 && rule.selectorText) { // CSSStyleRule
|
|
795
|
+
const sels = rule.selectorText.split(',').map(s => s.trim());
|
|
796
|
+
if (sels.some(testMatch)) {
|
|
797
|
+
matched.push(condition ? `@media ${condition} {\n ${rule.cssText}\n}` : rule.cssText);
|
|
798
|
+
}
|
|
799
|
+
} else if (rule.cssRules && (rule.type === 4 || rule.type === 12)) { // media / supports
|
|
800
|
+
walk(rule.cssRules, rule.conditionText || condition);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
for (const sheet of document.styleSheets) {
|
|
805
|
+
let rules;
|
|
806
|
+
try { rules = sheet.cssRules; } catch { continue; } // cross-origin sheet
|
|
807
|
+
if (rules) walk(rules, null);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const cls = (typeof el.className === 'string' && el.className.trim())
|
|
811
|
+
? '.' + el.className.trim().split(/\s+/).join('.') : '';
|
|
812
|
+
const desc = el.tagName.toLowerCase() + (el.id ? `#${el.id}` : '') + cls;
|
|
813
|
+
|
|
814
|
+
return { desc, computed, matched };
|
|
815
|
+
}, wantAll);
|
|
816
|
+
|
|
817
|
+
await handle.dispose();
|
|
818
|
+
|
|
819
|
+
const computedBlock = Object.entries(data.computed)
|
|
820
|
+
.map(([k, v]) => ` ${k}: ${v};`).join('\n');
|
|
821
|
+
const matchedBlock = data.matched.length
|
|
822
|
+
? data.matched.join('\n\n')
|
|
823
|
+
: '/* No authored rules matched — this element is styled via inline style or inherited rules only. */';
|
|
824
|
+
|
|
825
|
+
return {
|
|
826
|
+
content: [{
|
|
827
|
+
type: 'text',
|
|
828
|
+
text: [
|
|
829
|
+
`CSS for ${label}`,
|
|
830
|
+
`Matched element: ${data.desc}`,
|
|
831
|
+
'',
|
|
832
|
+
`### Authored rules that apply (${data.matched.length})`,
|
|
833
|
+
'```css',
|
|
834
|
+
matchedBlock,
|
|
835
|
+
'```',
|
|
836
|
+
'',
|
|
837
|
+
`### Key computed styles — resolved values (${Object.keys(data.computed).length})`,
|
|
838
|
+
'```css',
|
|
839
|
+
`${data.desc} {`,
|
|
840
|
+
computedBlock,
|
|
841
|
+
'}',
|
|
842
|
+
'```',
|
|
843
|
+
wantAll ? '' : '\n_Tip: pass `all: true` for the full computed style dump._',
|
|
844
|
+
].join('\n'),
|
|
845
|
+
}],
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
435
849
|
// ── reload ──────────────────────────────────────────────────────────
|
|
436
850
|
case 'reload': {
|
|
437
851
|
const page = await getPage();
|
|
438
852
|
await page.reload({ waitUntil: 'networkidle', timeout: 30_000 });
|
|
439
|
-
|
|
853
|
+
// The SPA mounts into `.app`; standalone CRM pages do not — wait only
|
|
854
|
+
// for the shell when we are on the SPA so reload works on both.
|
|
855
|
+
const isSpa = await page.evaluate(() => typeof window.__setScreen === 'function');
|
|
856
|
+
if (isSpa) await page.waitForSelector('.app', { timeout: 20_000 });
|
|
440
857
|
await page.waitForTimeout(500);
|
|
441
858
|
|
|
442
859
|
const png = await snap();
|
|
@@ -451,6 +868,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
451
868
|
// ── reset ───────────────────────────────────────────────────────────
|
|
452
869
|
case 'reset': {
|
|
453
870
|
const page = await getPage();
|
|
871
|
+
// May currently be on the CRM SPA — reload the Taskia shell first.
|
|
872
|
+
const onTaskia = await page.evaluate(() => typeof window.__setScreen === 'function' && window.__APP !== 'crm' && window.__APP !== 'pointer');
|
|
873
|
+
if (!onTaskia) {
|
|
874
|
+
await page.goto(PAGES_URL, { waitUntil: 'networkidle', timeout: 30_000 });
|
|
875
|
+
await page.waitForSelector('.app', { timeout: 20_000 });
|
|
876
|
+
}
|
|
454
877
|
await page.evaluate(() => {
|
|
455
878
|
window.__setSignedIn?.(true);
|
|
456
879
|
window.__setScreen?.('tasks');
|
|
@@ -470,10 +893,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
470
893
|
case 'list_screens': {
|
|
471
894
|
const source = await fetchAsset('screens.jsx');
|
|
472
895
|
const screens = [...source.matchAll(/^function\s+(Screen[A-Za-z]+)\s*\(/gm)].map(m => m[1]);
|
|
896
|
+
const crm = Object.keys(CRM_SCREENS);
|
|
897
|
+
const pointer = Object.keys(POINTER_SCREENS);
|
|
473
898
|
return {
|
|
474
899
|
content: [{
|
|
475
900
|
type: 'text',
|
|
476
|
-
text:
|
|
901
|
+
text:
|
|
902
|
+
`Taskia app — SPA screens (${screens.length}):\n` +
|
|
903
|
+
`${screens.map(s => ` • ${s}`).join('\n')}\n\n` +
|
|
904
|
+
`Pointer CRM — standalone SPA at ${CRM_APP_URL} (${crm.length}):\n` +
|
|
905
|
+
`${crm.map(s => ` • ${s} → screen "${CRM_SCREENS[s]}"`).join('\n')}\n\n` +
|
|
906
|
+
`Pointer ERP — standalone SPA at ${POINTER_APP_URL} (${pointer.length}):\n` +
|
|
907
|
+
`${pointer.map(s => ` • ${s} → screen "${POINTER_SCREENS[s]}"`).join('\n')}\n\n` +
|
|
908
|
+
`Use go_to_screen with any of these names.`,
|
|
477
909
|
}],
|
|
478
910
|
};
|
|
479
911
|
}
|