@theprototype/agent 1.53.0 → 1.54.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/catalog.json CHANGED
@@ -3,15 +3,24 @@
3
3
  "format": 2,
4
4
  "generator": {
5
5
  "source": "tpAgent.tools() via core scripts/agent-tool-catalog.cjs, mapped by tools/agent/src/catalog.js",
6
- "hostTools": 21,
6
+ "hostTools": 23,
7
7
  "dropped": []
8
8
  },
9
9
  "instructions": "You are building in the person's own open theprototype.app tab: a live, shared 3D scene. Every change shows for everyone in the session and can be undone.\nThis may be a VOICE conversation: answer in one or two short spoken sentences and say what changed in plain words (\"Added three trees by the fire\"). Never read out uuids, lists of numbers or JSON.\nIf a tool says the tab is closed or not connected, tell the person to open theprototype.app and press Connect Claude, then wait; do not retry in a loop.\nCall status first: it says whether you may edit (a viewer can only look).\nYou act as this person in their own tab: status \"you\" is them (their peer id, name and role); the other users are everyone else.\nCall list_scene before editing so you use real uuids; batch many objects into one call.\nFor \"in front of me\", \"here\" or \"this\", call where_are_users and build at the spots it gives, facing the person.\nMotion and interaction are flow nodes (create_flow_nodes); call describe_node_types before wiring edges.\nTake a screenshot after each change to check your work.\nWhen asked what you can do here, call what_can_you_do and say its \"say\" line.",
10
+ "connectorExcluded": [
11
+ "accept_offer",
12
+ "connect",
13
+ "import_file",
14
+ "import_model",
15
+ "install_modules",
16
+ "leave",
17
+ "pending_offers"
18
+ ],
10
19
  "tools": [
11
20
  {
12
21
  "name": "list_scene",
13
22
  "title": "Read the scene",
14
- "description": "List every object currently in the scene with its uuid, name, type, parent, transform, material, color and (for a primitive) its shape — geometry {type, sizes such as the thickness of a wall}. Call this before editing so you use real uuids. On a big scene narrow it: brief (uuids + names only), name (a glob), type, near {position, radius}. A big flow graph shows its first page and says how to read the rest: pass {flow: \"scene\" | object uuid, page} to read one page of one graph.\nIf a tool says the tab is closed or not connected, tell the person to open theprototype.app and press Connect Claude, then wait; do not retry in a loop.\nCall list_scene before editing so you use real uuids; batch many objects into one call.",
23
+ "description": "List every object currently in the scene with its uuid, name, type, parent, transform, material, color and (for a primitive) its shape — geometry {type, sizes such as the thickness of a wall}. The transform is the AUTHORED pose: an object an effect is moving also has animated: true, and live {position, rotation, scale, visible} when this frame differs; update_objects writes the authored pose, so never copy live back into it. Call this before editing so you use real uuids. On a big scene narrow it: brief (uuids + names only), name (a glob), type, near {position, radius}. A big flow graph shows its first page and says how to read the rest: pass {flow: \"scene\" | object uuid, page} to read one page of one graph.\nIf a tool says the tab is closed or not connected, tell the person to open theprototype.app and press Connect Claude, then wait; do not retry in a loop.\nCall list_scene before editing so you use real uuids; batch many objects into one call.",
15
24
  "inputSchema": {
16
25
  "type": "object",
17
26
  "properties": {
@@ -236,7 +245,7 @@
236
245
  {
237
246
  "name": "update_objects",
238
247
  "title": "Change objects",
239
- "description": "Modify existing objects by uuid: move/rotate/scale, recolor, change material, rename, show/hide, or reparent. Batch many in one call.\nCall list_scene before editing so you use real uuids; batch many objects into one call.\nTake a screenshot after each change to check your work.",
248
+ "description": "Modify existing objects by uuid: move/rotate/scale, recolor, change material, rename, show/hide, or reparent. Batch many in one call. On an object an effect is moving, a transform sets its authored pose (the one list_scene reports) and the effect carries on from there.\nCall list_scene before editing so you use real uuids; batch many objects into one call.\nTake a screenshot after each change to check your work.",
240
249
  "inputSchema": {
241
250
  "type": "object",
242
251
  "properties": {
@@ -529,7 +538,7 @@
529
538
  {
530
539
  "name": "create_flow_nodes",
531
540
  "title": "Add behaviour (flow) nodes",
532
- "description": "Add behavior (flow) nodes to a node graph — this is how objects get MOTION and interactivity: spin, bounce, patrol a path, pulse, blink, react to clicks. graph is \"scene\" or an object uuid (ONE graph per call). KEY RULE: a behavior node placed in an OBJECT's graph with no edges automatically drives that object — most behaviors are one node, zero edges, so usually OMIT edges. Describe nodes EITHER as text (graph lines, see the system prompt) OR as nodes/edges arrays. A call with some bad nodes still creates the good ones and returns partial:true + errors[] — resend only what errors[] names.\nMotion and interaction are flow nodes (create_flow_nodes); call describe_node_types before wiring edges.\nTake a screenshot after each change to check your work.",
541
+ "description": "Add behavior (flow) nodes to a node graph — this is how objects get MOTION and interactivity: spin, bounce, patrol a path, pulse, blink, react to clicks. graph is \"scene\" or an object uuid (ONE graph per call). KEY RULE: a behavior node placed in an OBJECT's graph with no edges automatically drives that object — most behaviors are one node, zero edges, so usually OMIT edges. A script node runs every frame FROM THE BASE POSE, so object.rotation.y += 0.02 lands at base + 0.02 and stays: write object.rotation.y = base.rot[1] + elapsed * speed (base has pos, rot, scale as arrays; elapsed = seconds since Play started, time = the shared clock, seconds of the day, a large number). Particle gravity is the acceleration along world Y: negative falls, positive rises. Describe nodes EITHER as text (graph lines, see the system prompt) OR as nodes/edges arrays. A call with some bad nodes still creates the good ones and returns partial:true + errors[] — resend only what errors[] names.\nMotion and interaction are flow nodes (create_flow_nodes); call describe_node_types before wiring edges.\nTake a screenshot after each change to check your work.",
533
542
  "inputSchema": {
534
543
  "type": "object",
535
544
  "properties": {
@@ -604,7 +613,7 @@
604
613
  {
605
614
  "name": "update_flow_nodes",
606
615
  "title": "Change or remove flow nodes",
607
- "description": "Tune or remove existing behavior nodes in one graph. Node ids and current data are in the scene summary under each object's \"flow\" (and \"sceneFlow\").\nTake a screenshot after each change to check your work.",
616
+ "description": "Tune or remove existing behavior nodes in one graph. Node ids and current data are in the scene summary under each object's \"flow\" (and \"sceneFlow\"). Script code runs every frame from the base pose: use object.rotation.y = base.rot[1] + elapsed * speed, not += (elapsed = seconds since Play started, time = the shared clock, seconds of the day).\nTake a screenshot after each change to check your work.",
608
617
  "inputSchema": {
609
618
  "type": "object",
610
619
  "properties": {
@@ -949,7 +958,7 @@
949
958
  },
950
959
  {
951
960
  "name": "import_model",
952
- "title": "Import model",
961
+ "title": "Import a 3D model",
953
962
  "description": "Import a self-contained GLB (binary glTF 2.0, everything embedded, ≤ 25 MB) into the scene through the app's own import pipeline (auto-optimize, LOD, budget check). Named nodes are kept as a hierarchy and returned with uuids. One undo step; syncs to every peer; provenance is recorded. Use for models built outside the app (e.g. an img2threejs rebuild exported with named pivots). Materials: standard PBR only — custom shaders do not survive glTF.\nTake a screenshot after each change to check your work.",
954
963
  "inputSchema": {
955
964
  "type": "object",
@@ -1005,15 +1014,15 @@
1005
1014
  }
1006
1015
  },
1007
1016
  "annotations": {
1008
- "title": "Import model",
1017
+ "title": "Import a 3D model",
1009
1018
  "readOnlyHint": false,
1010
- "destructiveHint": true,
1019
+ "destructiveHint": false,
1011
1020
  "openWorldHint": false
1012
1021
  }
1013
1022
  },
1014
1023
  {
1015
1024
  "name": "import_file",
1016
- "title": "Import file",
1025
+ "title": "Import a file",
1017
1026
  "description": "Bring a file made on your machine into the session: PNG / WebP images (≤ 16 MB, metadata stripped), JSON (≤ 2 MB) or a .tpanim clip. The type is read from the bytes. It lands in the Explorer (folder \"Generated\" unless `folder`), shared with every peer. Pass the generator's input as `spec` and it is saved beside the file as <name>.<generator>.json (e.g. knight.png + knight.pixling.json) so it can be regenerated. An image is also placed in the scene as a card (a plane; `place: false` to skip): `filter: \"nearest\"` keeps pixel art crisp (default when a spec is given), `frame: {w, h, col, row}` shows one frame of a sprite sheet, `facing: \"camera-y\"` turns it to face each viewer. One undo step; editors and admins only. Large files: send base64 parts with `chunk: {id, index, total}` (the bridge does this for you with `path`).\nTake a screenshot after each change to check your work.",
1018
1027
  "inputSchema": {
1019
1028
  "type": "object",
@@ -1141,9 +1150,9 @@
1141
1150
  }
1142
1151
  },
1143
1152
  "annotations": {
1144
- "title": "Import file",
1153
+ "title": "Import a file",
1145
1154
  "readOnlyHint": false,
1146
- "destructiveHint": true,
1155
+ "destructiveHint": false,
1147
1156
  "openWorldHint": false
1148
1157
  }
1149
1158
  },
@@ -1223,6 +1232,63 @@
1223
1232
  "openWorldHint": false
1224
1233
  }
1225
1234
  },
1235
+ {
1236
+ "name": "save_scene",
1237
+ "title": "Save the scene",
1238
+ "description": "Save the open scene as a new version in the person's Library, the same save as Ctrl+S; Version history can restore older ones. In someone else's session it saves on the host. Viewers cannot save, and a private scene cannot be saved this way.\nTake a screenshot after each change to check your work.",
1239
+ "inputSchema": {
1240
+ "type": "object",
1241
+ "properties": {
1242
+ "name": {
1243
+ "type": "string",
1244
+ "description": "scene name; omit to keep the open scene's name. Ignored when saving on a host."
1245
+ },
1246
+ "folder": {
1247
+ "type": "string",
1248
+ "description": "Library folder name or id; omit for the Library root. Ignored when saving on a host."
1249
+ }
1250
+ }
1251
+ },
1252
+ "annotations": {
1253
+ "title": "Save the scene",
1254
+ "readOnlyHint": false,
1255
+ "destructiveHint": false,
1256
+ "openWorldHint": false
1257
+ }
1258
+ },
1259
+ {
1260
+ "name": "generate_mesh",
1261
+ "title": "Generate a 3D model",
1262
+ "description": "Generate a NEW custom 3D mesh from a text description when no primitive fits (an organic prop, a detailed object, a character). SLOW (~1-3 min) and async — it returns immediately and the mesh appears in the scene when ready. Use sparingly; prefer primitives for simple/blocky shapes.\nTake a screenshot after each change to check your work.",
1263
+ "inputSchema": {
1264
+ "type": "object",
1265
+ "properties": {
1266
+ "prompt": {
1267
+ "type": "string",
1268
+ "description": "what to generate, e.g. \"a weathered wooden treasure chest\""
1269
+ },
1270
+ "name": {
1271
+ "type": "string"
1272
+ },
1273
+ "position": {
1274
+ "type": "array",
1275
+ "items": {
1276
+ "type": "number"
1277
+ },
1278
+ "description": "[x,y,z]"
1279
+ }
1280
+ },
1281
+ "required": [
1282
+ "prompt"
1283
+ ]
1284
+ },
1285
+ "annotations": {
1286
+ "title": "Generate a 3D model",
1287
+ "readOnlyHint": false,
1288
+ "destructiveHint": false,
1289
+ "openWorldHint": true
1290
+ }
1291
+ },
1226
1292
  {
1227
1293
  "name": "connect",
1228
1294
  "title": "Join a session",
@@ -1264,7 +1330,7 @@
1264
1330
  {
1265
1331
  "name": "screenshot",
1266
1332
  "title": "Take a picture of the scene",
1267
- "description": "See the scene: a PNG of a clean frame (no gizmos) from a temporary camera, never moving anyone's view. view: current | top | front | iso | focus (focus needs uuid).",
1333
+ "description": "See the scene: a PNG of a clean frame (no gizmos) from a temporary camera, never moving anyone's view. view: current | top | front | iso | focus (focus needs uuid or uuids); from = an angle or {yaw, pitch}; camera = a free {position, target}. 4:3 unless aspect says otherwise (current follows the person's window). Answers with the camera it used.",
1268
1334
  "inputSchema": {
1269
1335
  "type": "object",
1270
1336
  "properties": {
@@ -1280,7 +1346,93 @@
1280
1346
  },
1281
1347
  "uuid": {
1282
1348
  "type": "string",
1283
- "description": "object to frame (view focus)"
1349
+ "description": "object to frame"
1350
+ },
1351
+ "uuids": {
1352
+ "type": "array",
1353
+ "items": {
1354
+ "type": "string"
1355
+ },
1356
+ "description": "objects to frame together"
1357
+ },
1358
+ "from": {
1359
+ "description": "the angle around the subject, instead of view: front, back, left, right, top, below, iso, or {yaw, pitch} in degrees (yaw 0 = front, 90 = right; pitch 90 = from above)",
1360
+ "anyOf": [
1361
+ {
1362
+ "type": "string",
1363
+ "enum": [
1364
+ "front",
1365
+ "back",
1366
+ "left",
1367
+ "right",
1368
+ "top",
1369
+ "below",
1370
+ "iso"
1371
+ ]
1372
+ },
1373
+ {
1374
+ "type": "object",
1375
+ "properties": {
1376
+ "yaw": {
1377
+ "type": "number"
1378
+ },
1379
+ "pitch": {
1380
+ "type": "number"
1381
+ }
1382
+ },
1383
+ "required": [
1384
+ "yaw",
1385
+ "pitch"
1386
+ ]
1387
+ }
1388
+ ]
1389
+ },
1390
+ "camera": {
1391
+ "type": "object",
1392
+ "description": "a free camera, instead of view and from",
1393
+ "properties": {
1394
+ "position": {
1395
+ "type": "array",
1396
+ "items": {
1397
+ "type": "number"
1398
+ },
1399
+ "minItems": 3,
1400
+ "maxItems": 3,
1401
+ "description": "[x,y,z]"
1402
+ },
1403
+ "target": {
1404
+ "type": "array",
1405
+ "items": {
1406
+ "type": "number"
1407
+ },
1408
+ "minItems": 3,
1409
+ "maxItems": 3,
1410
+ "description": "[x,y,z]"
1411
+ }
1412
+ },
1413
+ "required": [
1414
+ "position",
1415
+ "target"
1416
+ ]
1417
+ },
1418
+ "aspect": {
1419
+ "description": "width to height: \"4:3\" (default), \"1:1\", \"16:9\" or a number. view current defaults to the person's window",
1420
+ "anyOf": [
1421
+ {
1422
+ "type": "string"
1423
+ },
1424
+ {
1425
+ "type": "number"
1426
+ }
1427
+ ]
1428
+ },
1429
+ "zoom": {
1430
+ "type": "number",
1431
+ "description": "above 1 is closer, 0.1 to 10"
1432
+ },
1433
+ "margin": {
1434
+ "type": "number",
1435
+ "description": "room around the subject: 1 touches the frame, default 1.15"
1284
1436
  },
1285
1437
  "size": {
1286
1438
  "type": "number",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theprototype/agent",
3
- "version": "1.53.0",
3
+ "version": "1.54.0",
4
4
  "description": "Let an AI agent (Claude Code, Claude Desktop, any MCP host) join a theprototype.app session from its link and build the 3D scene with you.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -357,13 +357,14 @@ export class BrowserEngine extends EventEmitter {
357
357
  fr.readAsDataURL(/** @type {Blob} */ (r));
358
358
  });
359
359
  }
360
- if (r && typeof r === 'object' && typeof r.data === 'string') return { data: r.data, mimeType: r.mimeType || 'image/png' };
361
- return typeof r === 'string' ? { data: r, mimeType: 'image/png' } : null;
360
+ const camera = typeof api.lastShot === 'function' ? api.lastShot() : null;
361
+ if (r && typeof r === 'object' && typeof r.data === 'string') return { data: r.data, mimeType: r.mimeType || 'image/png', camera };
362
+ return typeof r === 'string' ? { data: r, mimeType: 'image/png', camera } : null;
362
363
  }, opts)
363
364
  .catch((/** @type {any} */ e) => ({ error: firstLine(e) }));
364
365
  if (viaApi && typeof viaApi.data === 'string') {
365
366
  const m = /^data:([^;,]+)(;base64)?,/.exec(viaApi.data);
366
- return m ? { data: viaApi.data.slice(m[0].length), mimeType: m[1] } : viaApi;
367
+ return m ? { data: viaApi.data.slice(m[0].length), mimeType: m[1], camera: viaApi.camera } : viaApi;
367
368
  }
368
369
  const canvas = await this.page.evaluateHandle(() => {
369
370
  const all = [...document.querySelectorAll('canvas')].filter((c) => c.width > 0 && c.height > 0 && c.offsetParent !== null);
package/src/catalog.js CHANGED
@@ -33,6 +33,9 @@ export const TOOL_ANNOTATIONS = /** @type {Record<string, {title: string, read?:
33
33
  create_joints: { title: 'Add physics joints', destructive: false },
34
34
  group_objects: { title: 'Group objects', destructive: false },
35
35
  generate_mesh: { title: 'Generate a 3D model', destructive: false, openWorld: true },
36
+ save_scene: { title: 'Save the scene', destructive: false },
37
+ import_model: { title: 'Import a 3D model', destructive: false },
38
+ import_file: { title: 'Import a file', destructive: false },
36
39
  update_objects: { title: 'Change objects', destructive: true },
37
40
  update_flow_nodes: { title: 'Change or remove flow nodes', destructive: true },
38
41
  edit_behaviour: { title: 'Edit a code behaviour', destructive: true },
@@ -157,7 +160,7 @@ export function guidanceGaps(catalog) {
157
160
  /**
158
161
  * Host tools (tpAgent.tools()'s shape) → the static catalog.
159
162
  * @param {any[]} hostTools
160
- * @param {{provenance?: Record<string, any>}} [opts]
163
+ * @param {{provenance?: Record<string, any>, connectorExcluded?: string[]}} [opts]
161
164
  * @returns {{format: number, generator: Record<string, any>, instructions: string, tools: {name: string, title: string, description: string, inputSchema: any, annotations: Record<string, any>}[]}}
162
165
  */
163
166
  export function buildCatalog(hostTools, opts = {}) {
@@ -174,6 +177,26 @@ export function buildCatalog(hostTools, opts = {}) {
174
177
  ...(opts.provenance ?? {})
175
178
  },
176
179
  instructions: connectorInstructions(tools.map((t) => t.name)),
180
+ // mcp-fix: what the connector leaves out, on purpose (core's CONNECTOR_BLOCKED_TOOLS); the Worker checks against it
181
+ ...(opts.connectorExcluded ? { connectorExcluded: [...opts.connectorExcluded].sort() } : {}),
177
182
  tools
178
183
  };
179
184
  }
185
+
186
+ /**
187
+ * mcp-fix: the tab's list and the catalog must agree. Fails on a tool a connector tab answers that the catalog neither
188
+ * lists nor leaves out on purpose, and on a listed tool no connector tab can answer. `tabTools` comes from a real
189
+ * createConnectorAgent(); `conditional` = tools the tab offers only sometimes (status.missingTools says why).
190
+ * @param {{tabTools: string[], own: string[], conditional?: string[], catalogTools: string[], excluded: string[]}} facts
191
+ * @returns {string[]} the problems, in words
192
+ */
193
+ export function connectorToolGaps(facts) {
194
+ const { tabTools, own, conditional = [], catalogTools, excluded } = facts;
195
+ const out = new Set(excluded);
196
+ const listed = new Set(catalogTools.filter((n) => !out.has(n)));
197
+ const answered = new Set([...tabTools, ...own, ...conditional].filter((n) => !out.has(n)));
198
+ return [
199
+ ...[...answered].filter((n) => !listed.has(n)).map((n) => n + ': a connector tab answers it, but it is neither in the catalog nor in CONNECTOR_BLOCKED_TOOLS'),
200
+ ...[...listed].filter((n) => !answered.has(n)).map((n) => n + ': in the catalog, but no connector tab answers it')
201
+ ];
202
+ }
package/src/mcpServer.js CHANGED
@@ -222,10 +222,12 @@ export function createBridgeServer(opts) {
222
222
  if (!engine) return textResult({ error: 'not connected — join first', note: 'call connect with the session link first' }, true);
223
223
  if (name === 'screenshot' && reserved.includes('screenshot')) {
224
224
  if (!engineReady()) return textResult({ error: 'the agent page is not up yet (' + engine.state + ')' }, true);
225
- const shot = await engine.screenshot({ view: a.view, uuid: a.uuid, size: a.size });
225
+ const { view, uuid, uuids, from, camera, aspect, zoom, margin, size } = a;
226
+ const shot = await engine.screenshot({ view, uuid, uuids, from, camera, aspect, zoom, margin, size });
226
227
  /** @type {any[]} */
227
228
  const content = [{ type: 'image', data: shot.data, mimeType: shot.mimeType || 'image/png' }];
228
229
  if (shot.note) content.push({ type: 'text', text: shot.note });
230
+ if (shot.camera) content.push({ type: 'text', text: JSON.stringify({ camera: shot.camera }) });
229
231
  return { content };
230
232
  }
231
233
  if (name === 'wait_for_edit' && reserved.includes('wait_for_edit')) {
package/src/mcpTools.js CHANGED
@@ -86,12 +86,32 @@ export function bridgeTools(engine = 'browser') {
86
86
  {
87
87
  name: 'screenshot',
88
88
  description:
89
- 'See the scene: a PNG of a clean frame (no gizmos) from a temporary camera, never moving anyone\'s view. view: current | top | front | iso | focus (focus needs uuid).',
89
+ 'See the scene: a PNG of a clean frame (no gizmos) from a temporary camera, never moving anyone\'s view. view: current | top | front | iso | focus (focus needs uuid or uuids); from = an angle or {yaw, pitch}; camera = a free {position, target}. 4:3 unless aspect says otherwise (current follows the person\'s window). Answers with the camera it used.',
90
90
  inputSchema: {
91
91
  type: 'object',
92
92
  properties: {
93
93
  view: { type: 'string', enum: ['current', 'top', 'front', 'iso', 'focus'] },
94
- uuid: { type: 'string', description: 'object to frame (view focus)' },
94
+ uuid: { type: 'string', description: 'object to frame' },
95
+ uuids: { type: 'array', items: { type: 'string' }, description: 'objects to frame together' },
96
+ from: {
97
+ description: 'the angle around the subject, instead of view: front, back, left, right, top, below, iso, or {yaw, pitch} in degrees (yaw 0 = front, 90 = right; pitch 90 = from above)',
98
+ anyOf: [
99
+ { type: 'string', enum: ['front', 'back', 'left', 'right', 'top', 'below', 'iso'] },
100
+ { type: 'object', properties: { yaw: { type: 'number' }, pitch: { type: 'number' } }, required: ['yaw', 'pitch'] }
101
+ ]
102
+ },
103
+ camera: {
104
+ type: 'object',
105
+ description: 'a free camera, instead of view and from',
106
+ properties: {
107
+ position: { type: 'array', items: { type: 'number' }, minItems: 3, maxItems: 3, description: '[x,y,z]' },
108
+ target: { type: 'array', items: { type: 'number' }, minItems: 3, maxItems: 3, description: '[x,y,z]' }
109
+ },
110
+ required: ['position', 'target']
111
+ },
112
+ aspect: { description: 'width to height: "4:3" (default), "1:1", "16:9" or a number. view current defaults to the person\'s window', anyOf: [{ type: 'string' }, { type: 'number' }] },
113
+ zoom: { type: 'number', description: 'above 1 is closer, 0.1 to 10' },
114
+ margin: { type: 'number', description: 'room around the subject: 1 touches the frame, default 1.15' },
95
115
  size: { type: 'number', description: 'longest side in pixels' }
96
116
  }
97
117
  }