@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "Local bridge for InstaWebAI and Dexter — runs Codex, Claude Code, or OpenCode on your machine.",
5
5
  "type": "module",
6
6
  "bin": {
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
- allowedToolNames,
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(allowedToolNames)
72
+ ...(Array.isArray(toolDefinitions)
73
73
  ? {
74
- INSTAWEB_HARNESS_ALLOWED_TOOLS:
75
- JSON.stringify(allowedToolNames),
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.toolNames,
2464
+ harnessTools.definitions,
2465
2465
  ),
2466
2466
  }
2467
2467
  : null;
@@ -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
@@ -1,31 +1,68 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
- import { z } from 'zod';
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
- const configuredAllowedTools = (() => {
11
- const value = String(process.env.INSTAWEB_HARNESS_ALLOWED_TOOLS || '').trim();
12
- if (!value) return null;
16
+
17
+ function configuredToolDefinitions() {
18
+ let parsed;
13
19
  try {
14
- const parsed = JSON.parse(value);
15
- return Array.isArray(parsed)
16
- ? new Set(parsed.filter((name) => typeof name === 'string'))
17
- : null;
20
+ parsed = JSON.parse(
21
+ String(process.env.INSTAWEB_HARNESS_TOOL_DEFINITIONS || ''),
22
+ );
18
23
  } catch {
19
- return null;
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
- const toolAllowed = (name) =>
23
- !configuredAllowedTools || configuredAllowedTools.has(name);
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: result && typeof result === 'object' && !Array.isArray(result) ? result : { result },
87
+ structuredContent:
88
+ result && typeof result === 'object' && !Array.isArray(result)
89
+ ? result
90
+ : { result },
51
91
  };
52
92
  }
53
93
 
54
- const server = new McpServer({
55
- name: 'instawebai-harness',
56
- version: BRIDGE_VERSION,
57
- });
58
-
59
- if (toolAllowed('workspace_inspect')) server.registerTool(
60
- 'workspace_inspect',
61
- {
62
- description: 'Inspect workspace changes relative to the last synchronized InstaWebAI revision.',
63
- inputSchema: {},
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
- if (toolAllowed('framer_read_project')) server.registerTool(
190
- 'framer_read_project',
106
+ const server = new Server(
191
107
  {
192
- description:
193
- 'Run focused Framer Agent readProject queries against the connected project.',
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
- description:
206
- 'Apply Framer Agent DSL changes directly to the connected project.',
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
- if (toolAllowed('framer_read')) server.registerTool(
216
- 'framer_read',
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
- if (toolAllowed('framer_write')) server.registerTool(
228
- 'framer_write',
229
- {
230
- description:
231
- 'Execute project-scoped JavaScript with the official Framer Agent connection for mutations not covered by applyChanges.',
232
- inputSchema: {
233
- code: z.string().trim().min(1).max(200_000),
234
- },
235
- },
236
- async (args) => resultContent(await callGateway('framer_write', args)),
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);