@geometra/mcp 1.63.2 → 1.65.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 +29 -7
- package/dist/index.js +2 -5
- package/dist/proxy-spawn.d.ts +29 -0
- package/dist/proxy-spawn.js +49 -17
- package/dist/server.js +1557 -526
- package/dist/session-state.js +262 -80
- package/dist/session.d.ts +189 -13
- package/dist/session.js +1600 -319
- package/dist/state-privacy.d.ts +23 -0
- package/dist/state-privacy.js +171 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +32 -0
- package/package.json +4 -4
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type RetainedJsonValue = null | string | number | boolean | RetainedJsonValue[] | {
|
|
2
|
+
[key: string]: RetainedJsonValue;
|
|
3
|
+
};
|
|
4
|
+
export declare const REDACTED_STATE_VALUE = "[redacted]";
|
|
5
|
+
export declare const REDACTED_STATE_ERROR = "[redacted-error]";
|
|
6
|
+
export declare const REDACTED_STATE_PATH = "[redacted-path]";
|
|
7
|
+
export declare const REDACTED_STATE_URL = "[redacted-url]";
|
|
8
|
+
/**
|
|
9
|
+
* Retain only the network origin of a URL. Paths, queries, fragments, and
|
|
10
|
+
* credentials are deliberately discarded. Non-network and malformed URLs
|
|
11
|
+
* have no safe origin and are represented by a fixed redaction marker.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sanitizeUrlToOrigin(value: unknown): string | null;
|
|
14
|
+
/** Retain a bounded, machine-readable lifecycle code, never a raw message. */
|
|
15
|
+
export declare function sanitizeRetainedCode(value: unknown, fallback?: string): string;
|
|
16
|
+
/** Retain a structured error code while replacing free-form errors/messages. */
|
|
17
|
+
export declare function sanitizeRetainedError(value: unknown): string;
|
|
18
|
+
/**
|
|
19
|
+
* Convert arbitrary state into bounded JSON while defaulting all unknown
|
|
20
|
+
* strings to redaction. This is intentionally stricter than a JSON serializer:
|
|
21
|
+
* callers must use explicit metadata keys for the few strings worth retaining.
|
|
22
|
+
*/
|
|
23
|
+
export declare function sanitizeRetainedState(value: unknown): RetainedJsonValue;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
export const REDACTED_STATE_VALUE = '[redacted]';
|
|
2
|
+
export const REDACTED_STATE_ERROR = '[redacted-error]';
|
|
3
|
+
export const REDACTED_STATE_PATH = '[redacted-path]';
|
|
4
|
+
export const REDACTED_STATE_URL = '[redacted-url]';
|
|
5
|
+
const MAX_RETAINED_DEPTH = 8;
|
|
6
|
+
const MAX_RETAINED_ARRAY_ITEMS = 64;
|
|
7
|
+
const MAX_RETAINED_OBJECT_ENTRIES = 64;
|
|
8
|
+
const URL_KEY = /(?:^|[_-])(?:url|uri|href|location)(?:$|[_-])/i;
|
|
9
|
+
const ERROR_KEY = /(?:^|[_-])(?:error|errors|message|messages|stack|cause|exception|failure)(?:$|[_-])/i;
|
|
10
|
+
const PATH_KEY = /(?:^|[_-])(?:path|paths|file|files|filename|filenames|directory|dirname|cwd|root)(?:$|[_-])/i;
|
|
11
|
+
const SECRET_KEY = /(?:^|[_-])(?:password|passwd|passcode|secret|token|authorization|auth|cookie|credential|api[_-]?key|access[_-]?key|private[_-]?key)(?:$|[_-])/i;
|
|
12
|
+
const VALUE_KEY = /(?:^|[_-])(?:value|values|answer|answers|response|responses|content|text|body|result|results|payload|formdata|filled)(?:$|[_-])/i;
|
|
13
|
+
const SAFE_STRING_KEY = /^(?:sessionId|label|connectMode|transportMode|proxyStartMode|mode|status|reason|type|kind|scope|format)$/i;
|
|
14
|
+
const SAFE_PRIMITIVE_KEY = /^(?:isolated|proxyReusable|wsReadyState|updateRevision|hasLayout|hasTree|awaitInitialFrame|authenticatedProxyHandshake|lateInitialFrame|reconnectable|closeProxy|forceCloseProxy|reusedExistingSession|timeoutMs|proxyStartMs|connectMs|wsOpenMs|firstFrameMs|resizeKickoffMs|navigateMs|totalMs)$/i;
|
|
15
|
+
const SAFE_TOKEN = /^[a-z0-9][a-z0-9_.:-]{0,127}$/i;
|
|
16
|
+
const RETAINED_ERROR_CODES = new Set([
|
|
17
|
+
'connect_timeout',
|
|
18
|
+
'websocket_error',
|
|
19
|
+
'websocket_closed_before_ready',
|
|
20
|
+
'reconnect_failed',
|
|
21
|
+
]);
|
|
22
|
+
const URL_PREFIX = /^(?:https?|wss?):\/\//i;
|
|
23
|
+
const ABSOLUTE_PATH = /^(?:\/|[a-z]:[\\/]|\\\\)/i;
|
|
24
|
+
const SAFE_OBJECT_KEY = /^[a-z][a-z0-9_.-]{0,63}$/i;
|
|
25
|
+
/**
|
|
26
|
+
* Retain only the network origin of a URL. Paths, queries, fragments, and
|
|
27
|
+
* credentials are deliberately discarded. Non-network and malformed URLs
|
|
28
|
+
* have no safe origin and are represented by a fixed redaction marker.
|
|
29
|
+
*/
|
|
30
|
+
export function sanitizeUrlToOrigin(value) {
|
|
31
|
+
if (value === null || value === undefined || value === '')
|
|
32
|
+
return null;
|
|
33
|
+
if (typeof value !== 'string')
|
|
34
|
+
return REDACTED_STATE_URL;
|
|
35
|
+
try {
|
|
36
|
+
const parsed = new URL(value);
|
|
37
|
+
if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) {
|
|
38
|
+
return REDACTED_STATE_URL;
|
|
39
|
+
}
|
|
40
|
+
return parsed.origin === 'null' ? REDACTED_STATE_URL : parsed.origin;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return REDACTED_STATE_URL;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Retain a bounded, machine-readable lifecycle code, never a raw message. */
|
|
47
|
+
export function sanitizeRetainedCode(value, fallback = REDACTED_STATE_VALUE) {
|
|
48
|
+
return typeof value === 'string' && SAFE_TOKEN.test(value) ? value : fallback;
|
|
49
|
+
}
|
|
50
|
+
/** Retain a structured error code while replacing free-form errors/messages. */
|
|
51
|
+
export function sanitizeRetainedError(value) {
|
|
52
|
+
return typeof value === 'string' && RETAINED_ERROR_CODES.has(value)
|
|
53
|
+
? value
|
|
54
|
+
: REDACTED_STATE_ERROR;
|
|
55
|
+
}
|
|
56
|
+
function classifyKey(key) {
|
|
57
|
+
if (!key)
|
|
58
|
+
return 'unknown';
|
|
59
|
+
if (URL_KEY.test(key) || /(?:url|uri|href|location|origin)$/i.test(key))
|
|
60
|
+
return 'url';
|
|
61
|
+
if (ERROR_KEY.test(key) || /(?:error|message|stack|cause|exception|failure)$/i.test(key))
|
|
62
|
+
return 'error';
|
|
63
|
+
if (PATH_KEY.test(key) || /(?:path|file|filename|directory|dirname|cwd|root)$/i.test(key))
|
|
64
|
+
return 'path';
|
|
65
|
+
if (SECRET_KEY.test(key) || /(?:password|passwd|passcode|secret|token|authorization|auth|cookie|credential|apiKey|accessKey|privateKey)$/i.test(key))
|
|
66
|
+
return 'secret';
|
|
67
|
+
if (VALUE_KEY.test(key) || /(?:value|values|answer|answers|response|responses|content|text|body|result|results|payload|formdata|filled)$/i.test(key))
|
|
68
|
+
return 'value';
|
|
69
|
+
if (SAFE_STRING_KEY.test(key))
|
|
70
|
+
return 'safe';
|
|
71
|
+
if (SAFE_PRIMITIVE_KEY.test(key))
|
|
72
|
+
return 'primitive';
|
|
73
|
+
return 'unknown';
|
|
74
|
+
}
|
|
75
|
+
function sanitizeObjectKey(key, index) {
|
|
76
|
+
if (SAFE_OBJECT_KEY.test(key) &&
|
|
77
|
+
key !== '__proto__' &&
|
|
78
|
+
key !== 'prototype' &&
|
|
79
|
+
key !== 'constructor') {
|
|
80
|
+
return key;
|
|
81
|
+
}
|
|
82
|
+
return `redactedKey${index + 1}`;
|
|
83
|
+
}
|
|
84
|
+
function sanitizeString(value, key) {
|
|
85
|
+
switch (classifyKey(key)) {
|
|
86
|
+
case 'url':
|
|
87
|
+
return sanitizeUrlToOrigin(value);
|
|
88
|
+
case 'error':
|
|
89
|
+
return sanitizeRetainedError(value);
|
|
90
|
+
case 'path':
|
|
91
|
+
return REDACTED_STATE_PATH;
|
|
92
|
+
case 'secret':
|
|
93
|
+
case 'value':
|
|
94
|
+
return REDACTED_STATE_VALUE;
|
|
95
|
+
case 'safe':
|
|
96
|
+
return sanitizeRetainedCode(value);
|
|
97
|
+
default:
|
|
98
|
+
if (URL_PREFIX.test(value))
|
|
99
|
+
return sanitizeUrlToOrigin(value);
|
|
100
|
+
if (value.startsWith('file:') || ABSOLUTE_PATH.test(value))
|
|
101
|
+
return REDACTED_STATE_PATH;
|
|
102
|
+
return REDACTED_STATE_VALUE;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function sanitizeValue(value, key, depth, ancestors) {
|
|
106
|
+
if (value === null || value === undefined)
|
|
107
|
+
return null;
|
|
108
|
+
const classification = classifyKey(key);
|
|
109
|
+
if (classification === 'url') {
|
|
110
|
+
return sanitizeUrlToOrigin(value instanceof URL ? value.toString() : value);
|
|
111
|
+
}
|
|
112
|
+
if (classification === 'error') {
|
|
113
|
+
return typeof value === 'string' ? sanitizeRetainedError(value) : REDACTED_STATE_ERROR;
|
|
114
|
+
}
|
|
115
|
+
if (classification === 'path')
|
|
116
|
+
return REDACTED_STATE_PATH;
|
|
117
|
+
if (classification === 'secret' || classification === 'value')
|
|
118
|
+
return REDACTED_STATE_VALUE;
|
|
119
|
+
if (typeof value === 'boolean') {
|
|
120
|
+
return classification === 'primitive' ? value : REDACTED_STATE_VALUE;
|
|
121
|
+
}
|
|
122
|
+
if (typeof value === 'number') {
|
|
123
|
+
return classification === 'primitive' && Number.isFinite(value)
|
|
124
|
+
? value
|
|
125
|
+
: classification === 'primitive'
|
|
126
|
+
? null
|
|
127
|
+
: REDACTED_STATE_VALUE;
|
|
128
|
+
}
|
|
129
|
+
if (typeof value === 'string')
|
|
130
|
+
return sanitizeString(value, key);
|
|
131
|
+
if (value instanceof Date)
|
|
132
|
+
return REDACTED_STATE_VALUE;
|
|
133
|
+
if (value instanceof URL)
|
|
134
|
+
return sanitizeUrlToOrigin(value.toString());
|
|
135
|
+
if (value instanceof Error)
|
|
136
|
+
return REDACTED_STATE_ERROR;
|
|
137
|
+
if (typeof value !== 'object')
|
|
138
|
+
return REDACTED_STATE_VALUE;
|
|
139
|
+
if (depth >= MAX_RETAINED_DEPTH || ancestors.has(value))
|
|
140
|
+
return REDACTED_STATE_VALUE;
|
|
141
|
+
ancestors.add(value);
|
|
142
|
+
try {
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
return value
|
|
145
|
+
.slice(0, MAX_RETAINED_ARRAY_ITEMS)
|
|
146
|
+
.map(entry => sanitizeValue(entry, key, depth + 1, ancestors));
|
|
147
|
+
}
|
|
148
|
+
const prototype = Object.getPrototypeOf(value);
|
|
149
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
150
|
+
return REDACTED_STATE_VALUE;
|
|
151
|
+
}
|
|
152
|
+
const output = {};
|
|
153
|
+
const entries = Object.entries(value).slice(0, MAX_RETAINED_OBJECT_ENTRIES);
|
|
154
|
+
entries.forEach(([entryKey, entryValue], index) => {
|
|
155
|
+
const retainedKey = sanitizeObjectKey(entryKey, index);
|
|
156
|
+
output[retainedKey] = sanitizeValue(entryValue, entryKey, depth + 1, ancestors);
|
|
157
|
+
});
|
|
158
|
+
return output;
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
ancestors.delete(value);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Convert arbitrary state into bounded JSON while defaulting all unknown
|
|
166
|
+
* strings to redaction. This is intentionally stricter than a JSON serializer:
|
|
167
|
+
* callers must use explicit metadata keys for the few strings worth retaining.
|
|
168
|
+
*/
|
|
169
|
+
export function sanitizeRetainedState(value) {
|
|
170
|
+
return sanitizeValue(value, undefined, 0, new WeakSet());
|
|
171
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
const MCP_PACKAGE_NAME = '@geometra/mcp';
|
|
3
|
+
const SERVER_NAME = 'geometra';
|
|
4
|
+
const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
5
|
+
function readPackageVersion() {
|
|
6
|
+
const packageUrl = new URL('../package.json', import.meta.url);
|
|
7
|
+
let manifest;
|
|
8
|
+
try {
|
|
9
|
+
manifest = JSON.parse(readFileSync(packageUrl, 'utf8'));
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
13
|
+
throw new Error(`Unable to read ${MCP_PACKAGE_NAME} implementation metadata: ${reason}`, { cause: error });
|
|
14
|
+
}
|
|
15
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
16
|
+
throw new Error(`${MCP_PACKAGE_NAME} package.json must contain a JSON object`);
|
|
17
|
+
}
|
|
18
|
+
const record = manifest;
|
|
19
|
+
if (record.name !== MCP_PACKAGE_NAME) {
|
|
20
|
+
throw new Error(`${MCP_PACKAGE_NAME} package.json has an unexpected package name`);
|
|
21
|
+
}
|
|
22
|
+
if (typeof record.version !== 'string' || !SEMVER_PATTERN.test(record.version)) {
|
|
23
|
+
throw new Error(`${MCP_PACKAGE_NAME} package.json must contain a valid semantic version`);
|
|
24
|
+
}
|
|
25
|
+
return record.version;
|
|
26
|
+
}
|
|
27
|
+
export const GEOMETRA_MCP_VERSION = readPackageVersion();
|
|
28
|
+
/** MCP SDK implementation identity advertised during server initialization. */
|
|
29
|
+
export const SERVER_IMPLEMENTATION = Object.freeze({
|
|
30
|
+
name: SERVER_NAME,
|
|
31
|
+
version: GEOMETRA_MCP_VERSION,
|
|
32
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geometra/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.65.0",
|
|
4
4
|
"description": "MCP server for Geometra — interact with running Geometra apps via the geometry protocol, no browser needed",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"ui-testing"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@geometra/proxy": "^1.
|
|
35
|
+
"@geometra/proxy": "^1.65.0",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
37
37
|
"@razroo/parallel-mcp": "^0.1.0",
|
|
38
|
-
"ws": "^8.
|
|
39
|
-
"zod": "^3.
|
|
38
|
+
"ws": "^8.21.0",
|
|
39
|
+
"zod": "^4.3.6"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^22.0.0",
|