@gakim-digital/dexter-bridge 0.11.1 → 0.11.2
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 +1 -1
- package/src/agent.js +5 -5
- package/src/framerAgentTools.js +12 -0
- package/src/harnessMcpServer.js +91 -189
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -64,15 +64,15 @@ const HARNESS_MCP_SERVER_PATH = fileURLToPath(
|
|
|
64
64
|
export function harnessMcpProcessEnvironment(
|
|
65
65
|
gateway,
|
|
66
66
|
versions = process.versions,
|
|
67
|
-
|
|
67
|
+
toolDefinitions,
|
|
68
68
|
) {
|
|
69
69
|
return {
|
|
70
70
|
INSTAWEB_HARNESS_GATEWAY_URL: gateway.url,
|
|
71
71
|
INSTAWEB_HARNESS_GATEWAY_TOKEN: gateway.token,
|
|
72
|
-
...(Array.isArray(
|
|
72
|
+
...(Array.isArray(toolDefinitions)
|
|
73
73
|
? {
|
|
74
|
-
|
|
75
|
-
JSON.stringify(
|
|
74
|
+
INSTAWEB_HARNESS_TOOL_DEFINITIONS:
|
|
75
|
+
JSON.stringify(toolDefinitions),
|
|
76
76
|
}
|
|
77
77
|
: {}),
|
|
78
78
|
...(versions?.electron ? { ELECTRON_RUN_AS_NODE: '1' } : {}),
|
|
@@ -2461,7 +2461,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2461
2461
|
environment: harnessMcpProcessEnvironment(
|
|
2462
2462
|
gateway,
|
|
2463
2463
|
process.versions,
|
|
2464
|
-
harnessTools.
|
|
2464
|
+
harnessTools.definitions,
|
|
2465
2465
|
),
|
|
2466
2466
|
}
|
|
2467
2467
|
: null;
|
package/src/framerAgentTools.js
CHANGED
|
@@ -1029,6 +1029,18 @@ export function createFramerAgentToolRuntime({
|
|
|
1029
1029
|
allowedToolNames,
|
|
1030
1030
|
workingPreferences,
|
|
1031
1031
|
);
|
|
1032
|
+
const exposedToolNames = new Set(
|
|
1033
|
+
definitions.map((definition) => definition.name),
|
|
1034
|
+
);
|
|
1035
|
+
const missingToolNames = uniqueStrings(requestedTools)
|
|
1036
|
+
.filter((name) => !exposedToolNames.has(name));
|
|
1037
|
+
if (missingToolNames.length > 0) {
|
|
1038
|
+
throw toolError(
|
|
1039
|
+
'FRAMER_HARNESS_TOOL_CATALOG_INCOMPLETE',
|
|
1040
|
+
`Dexter could not start because this bridge does not expose the required Framer tools: ${missingToolNames.join(', ')}.`,
|
|
1041
|
+
500,
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1032
1044
|
const allowPublishing = assignment?.context?.permissions?.publish === true;
|
|
1033
1045
|
const inheritedRejectedMechanisms =
|
|
1034
1046
|
assignment?.context?.repairContext?.rejectedMechanisms
|
package/src/harnessMcpServer.js
CHANGED
|
@@ -1,31 +1,68 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ErrorCode,
|
|
8
|
+
ListToolsRequestSchema,
|
|
9
|
+
McpError,
|
|
10
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv';
|
|
6
12
|
import { BRIDGE_VERSION } from './config.js';
|
|
7
13
|
|
|
8
14
|
const gatewayUrl = String(process.env.INSTAWEB_HARNESS_GATEWAY_URL || '');
|
|
9
15
|
const gatewayToken = String(process.env.INSTAWEB_HARNESS_GATEWAY_TOKEN || '');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
|
|
17
|
+
function configuredToolDefinitions() {
|
|
18
|
+
let parsed;
|
|
13
19
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
: null;
|
|
20
|
+
parsed = JSON.parse(
|
|
21
|
+
String(process.env.INSTAWEB_HARNESS_TOOL_DEFINITIONS || ''),
|
|
22
|
+
);
|
|
18
23
|
} catch {
|
|
19
|
-
|
|
24
|
+
throw new Error('The InstaWebAI harness tool definitions are invalid.');
|
|
25
|
+
}
|
|
26
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
27
|
+
throw new Error('The InstaWebAI harness tool definitions are missing.');
|
|
20
28
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
const names = new Set();
|
|
30
|
+
return parsed.map((definition) => {
|
|
31
|
+
const name = String(definition?.name || '').trim();
|
|
32
|
+
const description = String(definition?.description || '').trim();
|
|
33
|
+
const inputSchema = definition?.inputSchema;
|
|
34
|
+
if (
|
|
35
|
+
!/^[A-Za-z0-9_.-]+$/.test(name)
|
|
36
|
+
|| names.has(name)
|
|
37
|
+
|| !description
|
|
38
|
+
|| !inputSchema
|
|
39
|
+
|| typeof inputSchema !== 'object'
|
|
40
|
+
|| Array.isArray(inputSchema)
|
|
41
|
+
|| inputSchema.type !== 'object'
|
|
42
|
+
) {
|
|
43
|
+
throw new Error('The InstaWebAI harness tool definitions are invalid.');
|
|
44
|
+
}
|
|
45
|
+
names.add(name);
|
|
46
|
+
return { name, description, inputSchema };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
24
49
|
|
|
25
50
|
if (!gatewayUrl || !gatewayToken) {
|
|
26
51
|
throw new Error('The InstaWebAI harness gateway is not configured.');
|
|
27
52
|
}
|
|
28
53
|
|
|
54
|
+
const toolDefinitions = configuredToolDefinitions();
|
|
55
|
+
const toolsByName = new Map(
|
|
56
|
+
toolDefinitions.map((definition) => [definition.name, definition]),
|
|
57
|
+
);
|
|
58
|
+
const schemaValidator = new AjvJsonSchemaValidator();
|
|
59
|
+
const argumentValidators = new Map(
|
|
60
|
+
toolDefinitions.map((definition) => [
|
|
61
|
+
definition.name,
|
|
62
|
+
schemaValidator.getValidator(definition.inputSchema),
|
|
63
|
+
]),
|
|
64
|
+
);
|
|
65
|
+
|
|
29
66
|
async function callGateway(name, args) {
|
|
30
67
|
const response = await fetch(gatewayUrl, {
|
|
31
68
|
method: 'POST',
|
|
@@ -47,194 +84,59 @@ async function callGateway(name, args) {
|
|
|
47
84
|
function resultContent(result) {
|
|
48
85
|
return {
|
|
49
86
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
50
|
-
structuredContent:
|
|
87
|
+
structuredContent:
|
|
88
|
+
result && typeof result === 'object' && !Array.isArray(result)
|
|
89
|
+
? result
|
|
90
|
+
: { result },
|
|
51
91
|
};
|
|
52
92
|
}
|
|
53
93
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
async () => resultContent(await callGateway('workspace_inspect', {})),
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
if (toolAllowed('workspace_sync')) server.registerTool(
|
|
69
|
-
'workspace_sync',
|
|
70
|
-
{
|
|
71
|
-
description: 'Synchronize all current workspace changes to the protected InstaWebAI build workspace.',
|
|
72
|
-
inputSchema: {},
|
|
73
|
-
},
|
|
74
|
-
async () => resultContent(await callGateway('workspace_sync', {})),
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
if (toolAllowed('shell_run')) server.registerTool(
|
|
78
|
-
'shell_run',
|
|
79
|
-
{
|
|
80
|
-
description:
|
|
81
|
-
'Run a project command in InstaWebAI’s isolated build workspace. Use it for package installation, code generation, tests, and builds.',
|
|
82
|
-
inputSchema: {
|
|
83
|
-
command: z.string().trim().min(1).max(8_192),
|
|
84
|
-
cwd: z
|
|
85
|
-
.string()
|
|
86
|
-
.regex(/^(?:\.|[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*)$/)
|
|
87
|
-
.max(500)
|
|
88
|
-
.default('.'),
|
|
89
|
-
timeoutMs: z.number().int().min(1_000).max(600_000).default(300_000),
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
async (args) => resultContent(await callGateway('shell_run', args)),
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
if (toolAllowed('preview_control')) server.registerTool(
|
|
96
|
-
'preview_control',
|
|
97
|
-
{
|
|
98
|
-
description: 'Start, restart, or inspect the server-managed preview for the current synchronized workspace.',
|
|
99
|
-
inputSchema: {
|
|
100
|
-
action: z.enum(['start', 'restart', 'status']),
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
async (args) => resultContent(await callGateway('preview_control', args)),
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
if (toolAllowed('browser_control')) server.registerTool(
|
|
107
|
-
'browser_control',
|
|
108
|
-
{
|
|
109
|
-
description:
|
|
110
|
-
'Drive an isolated browser against the current preview. Prefer batch for consecutive interactions and receive one compact final snapshot.',
|
|
111
|
-
inputSchema: {
|
|
112
|
-
action: z.enum(['open', 'snapshot', 'click', 'fill', 'select', 'press', 'batch', 'close']),
|
|
113
|
-
path: z.string().max(1_000).optional(),
|
|
114
|
-
selector: z.string().max(1_000).optional(),
|
|
115
|
-
value: z.string().max(10_000).optional(),
|
|
116
|
-
key: z.string().max(80).optional(),
|
|
117
|
-
actions: z.array(z.object({
|
|
118
|
-
action: z.enum(['open', 'snapshot', 'click', 'fill', 'select', 'press']),
|
|
119
|
-
path: z.string().max(1_000).optional(),
|
|
120
|
-
selector: z.string().max(1_000).optional(),
|
|
121
|
-
value: z.string().max(10_000).optional(),
|
|
122
|
-
key: z.string().max(80).optional(),
|
|
123
|
-
}).strict()).min(1).max(25).optional(),
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
async (args) => resultContent(await callGateway('browser_control', args)),
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
if (toolAllowed('data_inspect')) server.registerTool(
|
|
130
|
-
'data_inspect',
|
|
131
|
-
{
|
|
132
|
-
description:
|
|
133
|
-
'Read the application data contract or redacted development records without exposing database credentials.',
|
|
134
|
-
inputSchema: {
|
|
135
|
-
action: z.enum(['overview', 'list_records']),
|
|
136
|
-
entityId: z.string().max(180).optional(),
|
|
137
|
-
limit: z.number().int().min(1).max(100).default(50),
|
|
138
|
-
offset: z.number().int().min(0).max(100_000).default(0),
|
|
139
|
-
search: z.string().max(500).default(''),
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
async (args) => resultContent(await callGateway('data_inspect', args)),
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
if (toolAllowed('verification_run')) server.registerTool(
|
|
146
|
-
'verification_run',
|
|
147
|
-
{
|
|
148
|
-
description: 'Synchronize the workspace and run InstaWebAI deterministic and browser verification.',
|
|
149
|
-
inputSchema: {
|
|
150
|
-
level: z.enum(['fast', 'visual']),
|
|
151
|
-
workflowIds: z.array(z.string().max(180)).max(20).default([]),
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
async (args) => resultContent(await callGateway('verification_run', args)),
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
if (toolAllowed('progress_update')) server.registerTool(
|
|
158
|
-
'progress_update',
|
|
159
|
-
{
|
|
160
|
-
description:
|
|
161
|
-
'Send a brief user-facing progress update before the first inspection or edit and at meaningful phase changes.',
|
|
162
|
-
inputSchema: {
|
|
163
|
-
message: z.string().trim().min(1).max(500),
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
async (args) => resultContent(await callGateway('progress_update', args)),
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
if (toolAllowed('framer_instructions')) server.registerTool(
|
|
170
|
-
'framer_instructions',
|
|
171
|
-
{
|
|
172
|
-
description:
|
|
173
|
-
'Load Framer’s official agent command reference before making canvas changes.',
|
|
174
|
-
inputSchema: {},
|
|
175
|
-
},
|
|
176
|
-
async () => resultContent(await callGateway('framer_instructions', {})),
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
if (toolAllowed('framer_context')) server.registerTool(
|
|
180
|
-
'framer_context',
|
|
181
|
-
{
|
|
182
|
-
description:
|
|
183
|
-
'Read the connected Framer project context, including pages, components, styles, CMS, and the active branch.',
|
|
184
|
-
inputSchema: {},
|
|
185
|
-
},
|
|
186
|
-
async () => resultContent(await callGateway('framer_context', {})),
|
|
187
|
-
);
|
|
94
|
+
function errorContent(error) {
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: 'text',
|
|
99
|
+
text: error instanceof Error ? error.message : String(error),
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
isError: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
188
105
|
|
|
189
|
-
|
|
190
|
-
'framer_read_project',
|
|
106
|
+
const server = new Server(
|
|
191
107
|
{
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
inputSchema: {
|
|
195
|
-
queries: z.array(z.record(z.string(), z.unknown())).min(1).max(50),
|
|
196
|
-
pagePath: z.string().max(500).default('/'),
|
|
197
|
-
},
|
|
108
|
+
name: 'instawebai-harness',
|
|
109
|
+
version: BRIDGE_VERSION,
|
|
198
110
|
},
|
|
199
|
-
async (args) => resultContent(await callGateway('framer_read_project', args)),
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
if (toolAllowed('framer_apply_changes')) server.registerTool(
|
|
203
|
-
'framer_apply_changes',
|
|
204
111
|
{
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
inputSchema: {
|
|
208
|
-
changes: z.string().trim().min(1).max(250_000),
|
|
209
|
-
pagePath: z.string().max(500).default('/'),
|
|
112
|
+
capabilities: {
|
|
113
|
+
tools: {},
|
|
210
114
|
},
|
|
211
115
|
},
|
|
212
|
-
async (args) => resultContent(await callGateway('framer_apply_changes', args)),
|
|
213
116
|
);
|
|
214
117
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
description:
|
|
219
|
-
'Execute read-only JavaScript with the official Framer Agent connection.',
|
|
220
|
-
inputSchema: {
|
|
221
|
-
code: z.string().trim().min(1).max(200_000),
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
async (args) => resultContent(await callGateway('framer_read', args)),
|
|
225
|
-
);
|
|
118
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
119
|
+
tools: toolDefinitions,
|
|
120
|
+
}));
|
|
226
121
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
{
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
);
|
|
122
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
123
|
+
const { name, arguments: rawArguments = {} } = request.params;
|
|
124
|
+
if (!toolsByName.has(name)) {
|
|
125
|
+
throw new McpError(ErrorCode.InvalidParams, `Tool ${name} not found.`);
|
|
126
|
+
}
|
|
127
|
+
const validated = argumentValidators.get(name)(rawArguments);
|
|
128
|
+
if (!validated.valid) {
|
|
129
|
+
throw new McpError(
|
|
130
|
+
ErrorCode.InvalidParams,
|
|
131
|
+
`Invalid arguments for ${name}: ${validated.errorMessage}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
return resultContent(await callGateway(name, validated.data));
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return errorContent(error);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
238
140
|
|
|
239
141
|
const transport = new StdioServerTransport();
|
|
240
142
|
await server.connect(transport);
|