@geometra/mcp 1.25.0 → 1.27.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/dist/index.js +1 -1
- package/dist/server.js +13 -3
- package/dist/session.d.ts +5 -0
- package/dist/session.js +45 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import { performance } from 'node:perf_hooks';
|
|
|
3
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { formatConnectFailureMessage, isHttpUrl, normalizeConnectTarget } from './connect-utils.js';
|
|
6
|
-
import { connect, connectThroughProxy, disconnect, getSession, listSessions, getDefaultSessionId, prewarmProxy, sendClick, sendFillFields, sendType, sendKey, sendFileUpload, sendFieldText, sendFieldChoice, sendListboxPick, sendSelectOption, sendSetChecked, sendWheel, sendScreenshot, sendPdfGenerate, buildA11yTree, buildCompactUiIndex, buildFormRequiredSnapshot, buildPageModel, buildFormSchemas, expandPageSection, buildUiDelta, hasUiDelta, nodeIdForPath, nodeContextForNode, summarizeCompactIndex, summarizePageModel, summarizeUiDelta, waitForUiCondition, } from './session.js';
|
|
6
|
+
import { connect, connectThroughProxy, disconnect, getSession, listSessions, getDefaultSessionId, prewarmProxy, sendClick, sendFillFields, sendType, sendKey, sendFileUpload, sendFieldText, sendFieldChoice, sendListboxPick, sendSelectOption, sendSetChecked, sendWheel, sendScreenshot, sendPdfGenerate, buildA11yTree, buildCompactUiIndex, buildFormRequiredSnapshot, buildPageModel, buildFormSchemas, expandPageSection, buildUiDelta, hasUiDelta, nodeIdForPath, nodeContextForNode, parseSectionId, findNodeByPath, summarizeCompactIndex, summarizePageModel, summarizeUiDelta, waitForUiCondition, } from './session.js';
|
|
7
7
|
function checkedStateInput() {
|
|
8
8
|
return z
|
|
9
9
|
.union([z.boolean(), z.literal('mixed')])
|
|
@@ -1755,7 +1755,7 @@ Use this for dropdowns, location pickers, or any scrollable list where items are
|
|
|
1755
1755
|
maxScrollSteps: z.number().int().min(1).max(50).optional().default(20).describe('Max scroll steps before stopping (default 20)'),
|
|
1756
1756
|
scrollDelta: z.number().optional().default(300).describe('Vertical scroll delta per step (default 300)'),
|
|
1757
1757
|
sessionId: sessionIdInput,
|
|
1758
|
-
}, async ({ listId
|
|
1758
|
+
}, async ({ listId, role, scrollX, scrollY, maxItems, maxScrollSteps, scrollDelta, sessionId }) => {
|
|
1759
1759
|
const session = getSession(sessionId);
|
|
1760
1760
|
if (!session)
|
|
1761
1761
|
return err('Not connected. Call geometra_connect first.');
|
|
@@ -1767,7 +1767,17 @@ Use this for dropdowns, location pickers, or any scrollable list where items are
|
|
|
1767
1767
|
const a11y = await sessionA11yWhenReady(session);
|
|
1768
1768
|
if (!a11y)
|
|
1769
1769
|
break;
|
|
1770
|
-
|
|
1770
|
+
// Scope to subtree if listId is provided
|
|
1771
|
+
let searchRoot = a11y;
|
|
1772
|
+
if (listId) {
|
|
1773
|
+
const parsed = parseSectionId(listId);
|
|
1774
|
+
if (parsed) {
|
|
1775
|
+
const node = findNodeByPath(a11y, parsed.path);
|
|
1776
|
+
if (node)
|
|
1777
|
+
searchRoot = node;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
const items = findNodes(searchRoot, { role: itemRole });
|
|
1771
1781
|
let newCount = 0;
|
|
1772
1782
|
for (const item of items) {
|
|
1773
1783
|
const id = nodeIdForPath(item.path);
|
package/dist/session.d.ts
CHANGED
|
@@ -594,6 +594,10 @@ export declare function sendNavigate(session: Session, url: string, timeoutMs?:
|
|
|
594
594
|
*/
|
|
595
595
|
export declare function buildA11yTree(tree: Record<string, unknown>, layout: Record<string, unknown>): A11yNode;
|
|
596
596
|
export declare function nodeIdForPath(path: number[]): string;
|
|
597
|
+
export declare function parseSectionId(id: string): {
|
|
598
|
+
kind: PageSectionKind;
|
|
599
|
+
path: number[];
|
|
600
|
+
} | null;
|
|
597
601
|
/**
|
|
598
602
|
* Flat list of actionable / semantic nodes in the viewport, sorted with focusable first
|
|
599
603
|
* then top-to-bottom reading order. Intended to minimize LLM tokens vs a full nested tree.
|
|
@@ -608,6 +612,7 @@ export declare function buildCompactUiIndex(root: A11yNode, options?: {
|
|
|
608
612
|
context: CompactUiContext;
|
|
609
613
|
};
|
|
610
614
|
export declare function summarizeCompactIndex(nodes: CompactUiNode[], maxLines?: number): string;
|
|
615
|
+
export declare function findNodeByPath(root: A11yNode, path: number[]): A11yNode | null;
|
|
611
616
|
export declare function nodeContextForNode(root: A11yNode, node: A11yNode): NodeContextModel | undefined;
|
|
612
617
|
export declare function buildPageModel(root: A11yNode, options?: {
|
|
613
618
|
maxPrimaryActions?: number;
|
package/dist/session.js
CHANGED
|
@@ -8,6 +8,9 @@ let nextSessionId = 0;
|
|
|
8
8
|
function generateSessionId() { return `s${++nextSessionId}`; }
|
|
9
9
|
let reusableProxies = [];
|
|
10
10
|
const REUSABLE_PROXY_POOL_LIMIT = 6;
|
|
11
|
+
/** Close idle reusable proxies after 5 minutes of inactivity. */
|
|
12
|
+
const REUSABLE_PROXY_IDLE_TTL_MS = 5 * 60 * 1000;
|
|
13
|
+
let idleProxyTimer = null;
|
|
11
14
|
const trackedReusableProxyChildren = new WeakSet();
|
|
12
15
|
const ACTION_UPDATE_TIMEOUT_MS = 2000;
|
|
13
16
|
const LISTBOX_UPDATE_TIMEOUT_MS = 4500;
|
|
@@ -66,9 +69,11 @@ function closeReusableProxy(entry) {
|
|
|
66
69
|
catch {
|
|
67
70
|
/* ignore */
|
|
68
71
|
}
|
|
72
|
+
ensureIdleProxyTimer();
|
|
69
73
|
return;
|
|
70
74
|
}
|
|
71
75
|
void entry.runtime?.close().catch(() => { });
|
|
76
|
+
ensureIdleProxyTimer();
|
|
72
77
|
}
|
|
73
78
|
function closeReusableProxies() {
|
|
74
79
|
clearReusableProxiesIfExited();
|
|
@@ -87,6 +92,25 @@ function closeReusableProxies() {
|
|
|
87
92
|
void entry.runtime?.close().catch(() => { });
|
|
88
93
|
}
|
|
89
94
|
}
|
|
95
|
+
function evictIdleReusableProxies() {
|
|
96
|
+
clearReusableProxiesIfExited();
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
const stale = reusableProxies.filter(entry => !reusableProxyEntryIsActive(entry) && (now - entry.lastUsedAt) > REUSABLE_PROXY_IDLE_TTL_MS);
|
|
99
|
+
for (const entry of stale) {
|
|
100
|
+
closeReusableProxy(entry);
|
|
101
|
+
}
|
|
102
|
+
ensureIdleProxyTimer();
|
|
103
|
+
}
|
|
104
|
+
function ensureIdleProxyTimer() {
|
|
105
|
+
if (reusableProxies.length > 0 && !idleProxyTimer) {
|
|
106
|
+
idleProxyTimer = setInterval(evictIdleReusableProxies, 60_000);
|
|
107
|
+
idleProxyTimer.unref();
|
|
108
|
+
}
|
|
109
|
+
else if (reusableProxies.length === 0 && idleProxyTimer) {
|
|
110
|
+
clearInterval(idleProxyTimer);
|
|
111
|
+
idleProxyTimer = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
90
114
|
function enforceReusableProxyPoolLimit() {
|
|
91
115
|
clearReusableProxiesIfExited();
|
|
92
116
|
if (reusableProxies.length <= REUSABLE_PROXY_POOL_LIMIT)
|
|
@@ -139,6 +163,7 @@ function setReusableProxy(proxy, wsUrl, opts) {
|
|
|
139
163
|
child.once('error', clear);
|
|
140
164
|
}
|
|
141
165
|
enforceReusableProxyPoolLimit();
|
|
166
|
+
ensureIdleProxyTimer();
|
|
142
167
|
return;
|
|
143
168
|
}
|
|
144
169
|
reusableProxies.push({
|
|
@@ -153,6 +178,7 @@ function setReusableProxy(proxy, wsUrl, opts) {
|
|
|
153
178
|
lastUsedAt: now,
|
|
154
179
|
});
|
|
155
180
|
enforceReusableProxyPoolLimit();
|
|
181
|
+
ensureIdleProxyTimer();
|
|
156
182
|
}
|
|
157
183
|
function rememberReusableProxyPageUrl(session) {
|
|
158
184
|
const entry = reusableProxyEntryForSession(session);
|
|
@@ -1046,7 +1072,7 @@ function sectionPrefix(kind) {
|
|
|
1046
1072
|
function sectionIdForPath(kind, path) {
|
|
1047
1073
|
return `${sectionPrefix(kind)}:${encodePath(path)}`;
|
|
1048
1074
|
}
|
|
1049
|
-
function parseSectionId(id) {
|
|
1075
|
+
export function parseSectionId(id) {
|
|
1050
1076
|
const [prefix, encoded] = id.split(':', 2);
|
|
1051
1077
|
if (!prefix || !encoded)
|
|
1052
1078
|
return null;
|
|
@@ -1314,7 +1340,7 @@ function firstNamedDescendant(node, allowedRoles) {
|
|
|
1314
1340
|
}
|
|
1315
1341
|
return undefined;
|
|
1316
1342
|
}
|
|
1317
|
-
function findNodeByPath(root, path) {
|
|
1343
|
+
export function findNodeByPath(root, path) {
|
|
1318
1344
|
let current = root;
|
|
1319
1345
|
for (const index of path) {
|
|
1320
1346
|
if (!current.children[index])
|
|
@@ -2054,10 +2080,25 @@ export function buildPageModel(root, options) {
|
|
|
2054
2080
|
};
|
|
2055
2081
|
}
|
|
2056
2082
|
export function buildFormSchemas(root, options) {
|
|
2057
|
-
const
|
|
2083
|
+
const explicitForms = [
|
|
2058
2084
|
...(root.role === 'form' ? [root] : []),
|
|
2059
2085
|
...collectDescendants(root, candidate => candidate.role === 'form'),
|
|
2060
|
-
]
|
|
2086
|
+
];
|
|
2087
|
+
// Infer forms from group/region containers with 2+ form fields (e.g. Ashby-style UIs without <form>)
|
|
2088
|
+
const inferredForms = collectDescendants(root, candidate => {
|
|
2089
|
+
if (candidate.role !== 'group' && candidate.role !== 'region')
|
|
2090
|
+
return false;
|
|
2091
|
+
// Skip descendants of explicit forms
|
|
2092
|
+
for (const form of explicitForms) {
|
|
2093
|
+
if (candidate.path.length > form.path.length &&
|
|
2094
|
+
form.path.every((v, i) => candidate.path[i] === v)) {
|
|
2095
|
+
return false;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
const fields = collectDescendants(candidate, child => FORM_FIELD_ROLES.has(child.role));
|
|
2099
|
+
return fields.length >= 2;
|
|
2100
|
+
});
|
|
2101
|
+
const forms = sortByBounds([...explicitForms, ...inferredForms]);
|
|
2061
2102
|
return forms
|
|
2062
2103
|
.filter(form => !options?.formId || sectionIdForPath('form', form.path) === options.formId)
|
|
2063
2104
|
.map(form => buildFormSchemaForNode(root, form, options));
|
package/package.json
CHANGED