@bifocal/mcp 0.1.7 → 0.1.8

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.
@@ -183,3 +183,15 @@ export async function importPrototypeConfirm(projectId, prototypeId, contextId,
183
183
  }
184
184
  return response.json();
185
185
  }
186
+ export async function screenshotPrototype(prototypeId, path, interactionSteps) {
187
+ const response = await fetch(`${API_URL}/api/prototypes/${prototypeId}/screenshot`, {
188
+ method: 'POST',
189
+ headers: headers(),
190
+ body: JSON.stringify({ path, interaction_steps: interactionSteps }),
191
+ });
192
+ if (!response.ok) {
193
+ const error = await response.json().catch(() => ({}));
194
+ throw new Error(error.error || `Request failed: ${response.status}`);
195
+ }
196
+ return response.json();
197
+ }
package/dist/index.js CHANGED
@@ -1,43 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
- import { createProject, updateProject, listProjects, importPrototypeUploadUrl, importPrototypeConfirm, addFeedbackText, generateSolution, getInsights, getQuotes, getSolutions, getSolution, getPrototypes, getPrototype, exportPrototype, generatePrototype, updatePrototype, getContexts, createContext, createSolution, updateSolution } from './bifocalClient.js';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
+ import { createProject, updateProject, listProjects, importPrototypeUploadUrl, importPrototypeConfirm, addFeedbackText, generateSolution, getInsights, getQuotes, getSolutions, getSolution, getPrototypes, getPrototype, exportPrototype, generatePrototype, updatePrototype, getContexts, createContext, createSolution, updateSolution, screenshotPrototype } from './bifocalClient.js';
6
6
  import { writeFile } from 'fs/promises';
7
7
  import { join } from 'path';
8
- async function pollUntilReady(fn, isDone, intervalMs, timeoutMs) {
9
- const deadline = Date.now() + timeoutMs;
10
- while (true) {
11
- const result = await fn();
12
- if (isDone(result))
13
- return { result, timedOut: false };
14
- if (Date.now() + intervalMs >= deadline)
15
- return { result, timedOut: true };
16
- await new Promise(resolve => setTimeout(resolve, intervalMs));
17
- }
18
- }
19
- const server = new Server({ name: 'bifocal', version: '0.1.0' }, { capabilities: { tools: {}, resources: {} } });
8
+ const server = new Server({ name: 'bifocal', version: '0.1.0' }, { capabilities: { tools: {} } });
20
9
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
21
10
  tools: [
22
11
  {
23
12
  name: 'import_prototype',
24
- description: `Import a local zip file as a prototype into a Bifocal project.
25
-
26
- If prototype_id is provided, the zip is uploaded to update an existing prototype (e.g. after building or editing locally via the client coding agent). The existing prototype record is reused and re-processed — no new record is created.
27
-
28
- ## Preparing the zip
29
-
30
- **Claude Code / other agents building locally:**
31
- - Do not modify any files in the existing codebase. Read existing files to understand the experience, but never edit them. Create all prototype files in a new separate directory.
32
- - Bifocal requires a Vite + React single-page app with react-router-dom so that each major section has its own URL path. Next.js is not supported.
33
- - Replace any API calls or environment variables with hardcoded fixture data. The prototype must work with no backend and no network requests.
34
- - Stub out external services — auth, feature flags, analytics, payments, real-time connections — with simple no-op replacements.
35
- - Run \`npm run build\` and fix any errors before zipping.
36
- - ZIP the project excluding node_modules, .git, dist, and any .env files or credentials.
37
-
38
- **Lovable:** open your project → top-right menu → Export → Download ZIP. Do not manually zip the files.
39
-
40
- **Figma Make:** go to Prototype Settings → GitHub, push to a GitHub repo, then download the ZIP from GitHub (Code → Download ZIP). Note: some Figma assets may not convert correctly — check asset paths if images are missing.`,
13
+ description: 'Import a local zip file as a prototype into a Bifocal project. Always call get_import_instructions first to ensure the zip is correctly prepared.\n\nIf prototype_id is provided, the zip is uploaded to update an existing prototype (e.g. after building or editing locally via the client coding agent). The existing prototype record is reused and re-processed — no new record is created.',
41
14
  inputSchema: {
42
15
  type: 'object',
43
16
  properties: {
@@ -63,6 +36,21 @@ If prototype_id is provided, the zip is uploaded to update an existing prototype
63
36
  required: ['project_id', 'prototype_id', 'source'],
64
37
  },
65
38
  },
39
+ {
40
+ name: 'get_import_instructions',
41
+ description: 'Get platform-specific instructions for preparing a prototype zip file before importing it into Bifocal. Always call this before import_prototype.',
42
+ inputSchema: {
43
+ type: 'object',
44
+ properties: {
45
+ platform: {
46
+ type: 'string',
47
+ enum: ['claudecode', 'lovable', 'figmamake', 'other'],
48
+ description: 'The platform the prototype was built with.',
49
+ },
50
+ },
51
+ required: ['platform'],
52
+ },
53
+ },
66
54
  {
67
55
  name: 'create_project',
68
56
  description: 'Create a new Bifocal project. Before calling this tool, always ask the user if they have a PRD or any context to add for the project.',
@@ -118,7 +106,7 @@ If prototype_id is provided, the zip is uploaded to update an existing prototype
118
106
  },
119
107
  {
120
108
  name: 'create_context',
121
- description: 'Create or update a context for a Bifocal project. Contexts are attached to a solution to provide additional reference material (e.g. product docs, design system, brand guidelines) during generation. If a context with the same name and type already exists for the project\'s organization, it will be updated.\n\nIf you\'re not sure whether a context with this name already exists, call get_contexts first — an existing match will be silently overwritten.',
109
+ description: 'Create or update a context for a Bifocal project. Contexts are attached to a solution to provide additional reference material (e.g. product docs, design system, brand guidelines) during generation. If a context with the same name and type already exists for the project\'s organization, it will be updated.',
122
110
  inputSchema: {
123
111
  type: 'object',
124
112
  properties: {
@@ -144,7 +132,7 @@ If prototype_id is provided, the zip is uploaded to update an existing prototype
144
132
  },
145
133
  {
146
134
  name: 'generate_solution',
147
- description: 'Generate a new solution for a project based on selected insights. Before calling this tool, always: (1) call get_insights to show the user the available insights, (2) ask the user which insights they want to prioritize, (3) ask if they have a specific goal or any constraints for the solution, (4) optionally call get_contexts and ask the user if they want to attach any context. Only call this tool once you have that input. Blocks until the solution is ready and returns the full detail. The base prototype is resolved automatically.',
135
+ description: 'Generate a new solution for a project based on selected insights. Before calling this tool, always: (1) call get_insights to show the user the available insights, (2) ask the user which insights they want to prioritize, (3) ask if they have a specific goal or any constraints for the solution, (4) optionally call get_contexts and ask the user if they want to attach any context. Only call this tool once you have that input. This is async use get_solutions to check when the solution appears. The base prototype is resolved automatically.',
148
136
  inputSchema: {
149
137
  type: 'object',
150
138
  properties: {
@@ -159,7 +147,7 @@ If prototype_id is provided, the zip is uploaded to update an existing prototype
159
147
  },
160
148
  {
161
149
  name: 'create_solution',
162
- description: 'Directly insert a fully-specified solution into a Bifocal project. Use this when you already know exactly what the solution should be, rather than having it generated. The solution must follow the same schema as generated solutions. The base prototype is resolved automatically.\n\nBefore calling this tool you MUST:\n1. Call get_insights — insight_ids must be real IDs from that response. Never fabricate or guess insight IDs.\n2. Call get_prototypes with the base prototype_id — pages_to_modify must be real page paths that exist in the prototype sitemap. pages_to_create must be genuinely new paths not already in the sitemap. Never fabricate page paths.\n\nAll fields are required. barriers_addressed must reflect actual barriers from the selected insights. interventions must each map to a real barrier. Do not submit this tool call until every field has been populated with valid, verified data.\n\n**IMPORTANT**: Never fabricate or guess insight_ids or page paths in interventions — they must come from a real tool response. A made-up insight ID will silently link to nothing. A made-up page path will send the coding agent to a page that doesn\'t exist.\n\n<example description="A solution with one intervention and two changes">\n{\n "project_id": "proj_abc123",\n "title": "Condensed Flow with Inline Price Transparency",\n "brief": "Surfaces pricing and meal frequency options earlier in onboarding so users can evaluate value before committing. Reduces drop-off at the plan selection step.",\n "category": "flow",\n "barriers_addressed": [\n "Users don\'t understand what\'s included in the price before they reach checkout",\n "Frequency options feel buried and hard to compare"\n ],\n "interventions": [\n {\n "barrier": "Users don\'t understand what\'s included in the price before they reach checkout",\n "changes": [\n {\n "approach": "Add frequency price cards to step 13",\n "description": "Replace the single plan summary with 3 side-by-side cards showing weekly, biweekly, and monthly frequency. Each card shows: price per delivery, total weekly cost, and a \'most popular\' badge on the biweekly option. Tapping a card selects it and updates the CTA label to reflect the chosen frequency.",\n "pages_to_modify": ["/onboarding/step-13"],\n "pages_to_create": []\n },\n {\n "approach": "Add a \'What\'s included\' expandable section to step 13",\n "description": "Below the frequency cards, add a collapsed accordion labeled \'What\'s included in every box\'. Expanding it shows: number of recipes, serving sizes, packaging info, and a skip-anytime note. Default state is collapsed.",\n "pages_to_modify": ["/onboarding/step-13"],\n "pages_to_create": []\n }\n ]\n }\n ],\n "assumptions": [\n "Users can see and compare all three frequency options without scrolling on mobile"\n ],\n "open_concerns": [\n "Does surfacing price earlier increase or decrease conversion? Should be A/B tested."\n ],\n "insight_ids": ["ins_111", "ins_222"]\n}\n</example>',
150
+ description: 'Directly insert a fully-specified solution into a Bifocal project. Use this when you already know exactly what the solution should be, rather than having it generated. The solution must follow the same schema as generated solutions. The base prototype is resolved automatically.\n\nBefore calling this tool you MUST:\n1. Call get_insights — insight_ids must be real IDs from that response. Never fabricate or guess insight IDs.\n2. Call get_prototype on the base prototype — pages_to_modify must be real page paths that exist in the prototype sitemap. pages_to_create must be genuinely new paths not already in the sitemap. Never fabricate page paths.\n\nAll fields are required. barriers_addressed must reflect actual barriers from the selected insights. interventions must each map to a real barrier. Do not submit this tool call until every field has been populated with valid, verified data.',
163
151
  inputSchema: {
164
152
  type: 'object',
165
153
  properties: {
@@ -202,7 +190,7 @@ If prototype_id is provided, the zip is uploaded to update an existing prototype
202
190
  },
203
191
  {
204
192
  name: 'update_solution',
205
- description: 'Update an existing solution in a Bifocal project. All fields are optional — only send what needs to change. The solution schema must remain valid after the update.\n\nBefore calling this tool you MUST:\n1. Call get_solutions with solution_id to read the current state. Verify solution_generation_status is "completed" — do not call this tool if the solution is still generating.\n2. If updating insight_ids: call get_insights first and use only real IDs from that response. Never fabricate or guess insight IDs.\n3. If updating interventions (and therefore page paths): call get_prototypes with the base prototype_id to confirm pages_to_modify exist in the sitemap and pages_to_create are genuinely new.\n\nDo not submit partial or invalid data. Every field you include must be fully valid and complete.\n\n**IMPORTANT**: interventions is a full replacement — include every intervention, not just the ones that changed. Omitting an existing intervention will delete it. interventions is a full replacement — include all interventions, not just the ones that changed.\n\n<example description="Updating only the interventions on an existing solution">\n{\n "project_id": "proj_abc123",\n "solution_id": "sol_xyz789",\n "interventions": [\n {\n "barrier": "Users don\'t understand what\'s included in the price before they reach checkout",\n "changes": [\n {\n "approach": "Add frequency price cards to step 13",\n "description": "Replace the single plan summary with 3 side-by-side cards showing weekly, biweekly, and monthly frequency. Each card shows price per delivery and total weekly cost.",\n "pages_to_modify": ["/onboarding/step-13"],\n "pages_to_create": []\n }\n ]\n }\n ]\n}\n</example>',
193
+ description: 'Update an existing solution in a Bifocal project. All fields are optional — only send what needs to change. The solution schema must remain valid after the update.\n\nBefore calling this tool you MUST:\n1. Call get_solution to read the current state. Verify solution_generation_status is "completed" — do not call this tool if the solution is still generating.\n2. If updating insight_ids: call get_insights first and use only real IDs from that response. Never fabricate or guess insight IDs.\n3. If updating interventions (and therefore page paths): call get_prototype on the base prototype to confirm pages_to_modify exist in the sitemap and pages_to_create are genuinely new.\n\nDo not submit partial or invalid data. Every field you include must be fully valid and complete.',
206
194
  inputSchema: {
207
195
  type: 'object',
208
196
  properties: {
@@ -245,19 +233,30 @@ If prototype_id is provided, the zip is uploaded to update an existing prototype
245
233
  },
246
234
  {
247
235
  name: 'get_solutions',
248
- description: 'Get solutions for a Bifocal project. If solution_id is provided, returns full detail for that solution including all interventions and page changes. If omitted, returns a summary list of all solutions (title, brief, barriers addressed, prototype link).',
236
+ description: 'Get all solutions for a Bifocal project. Returns title, brief, barriers addressed, and prototype link (if one was generated) for each solution.',
249
237
  inputSchema: {
250
238
  type: 'object',
251
239
  properties: {
252
- project_id: { type: 'string', description: 'The ID of the project.' },
253
- solution_id: { type: 'string', description: 'Optional. If provided, returns full detail for this specific solution.' },
240
+ project_id: { type: 'string', description: 'The ID of the project to fetch solutions for.' },
254
241
  },
255
242
  required: ['project_id'],
256
243
  },
257
244
  },
245
+ {
246
+ name: 'get_solution',
247
+ description: 'Get full detail for a specific solution including all proposed interventions and page changes. Call this when the user wants to understand what a solution proposes to change.',
248
+ inputSchema: {
249
+ type: 'object',
250
+ properties: {
251
+ project_id: { type: 'string', description: 'The ID of the project the solution belongs to.' },
252
+ solution_id: { type: 'string', description: 'The ID of the solution to fetch.' },
253
+ },
254
+ required: ['project_id', 'solution_id'],
255
+ },
256
+ },
258
257
  {
259
258
  name: 'generate_prototype',
260
- description: 'Generate a prototype from a solution.\n\nBefore calling, make sure you understand what the solution does — its interventions, the pages it targets, and the barriers it addresses. This helps you evaluate the result and write useful edits if needed. If you haven\'t read the full detail yet, call get_solutions with the solution_id first.\n\n**IMPORTANT**: Don\'t call this while the solution is still generating — check that solution_generation_status is completed first.\n\nIf coding_agent is "bifocal" (default): queues a build and blocks until the prototype is ready, then returns the full prototype detail.\n\nIf coding_agent is "client": returns the full solution spec, base prototype ID, and org contexts so the client can build the prototype locally. After building, call import_prototype with the returned prototype_id to upload and link. If a prototype already exists for the solution, returns it immediately regardless of coding_agent.',
259
+ description: 'Generate a prototype from a solution.\n\nIf coding_agent is "bifocal" (default): queues an async build poll get_prototype until status is "ready".\n\nIf coding_agent is "client": returns the full solution spec, base prototype ID, and org contexts so the client can build the prototype locally. After building, call import_prototype with the returned prototype_id to upload and link. If a prototype already exists for the solution, returns it immediately regardless of coding_agent.',
261
260
  inputSchema: {
262
261
  type: 'object',
263
262
  properties: {
@@ -270,30 +269,40 @@ If prototype_id is provided, the zip is uploaded to update an existing prototype
270
269
  },
271
270
  {
272
271
  name: 'update_prototype',
273
- description: 'Edit an existing prototype.\n\nYour edit instruction should reference real pages that exist in the prototype. Make sure you know the sitemap before writing it — if you\'re not sure what pages exist, call get_prototypes with the prototype_id first.\n\nIf coding_agent is "bifocal" (default): sends the edit instruction to Bifocal\'s coding agent and blocks until the prototype is ready, then returns the full prototype detail.\n\nIf coding_agent is "client": returns the edit instruction and prototype_id so the client can make the changes locally. Export the prototype first via export_prototype, apply the changes, then call import_prototype with prototype_id to upload the updated zip.',
272
+ description: 'Edit an existing prototype.\n\nIf coding_agent is "bifocal" (default): sends the edit instruction to Bifocal\'s coding agent async, poll get_prototype until status returns to "ready".\n\nIf coding_agent is "client": returns the edit instruction and prototype_id so the client can make the changes locally. Export the prototype first via export_prototype, apply the changes, then call import_prototype with prototype_id to upload the updated zip.',
274
273
  inputSchema: {
275
274
  type: 'object',
276
275
  properties: {
277
- project_id: { type: 'string', description: 'The ID of the project the prototype belongs to.' },
278
276
  prototype_id: { type: 'string', description: 'The ID of the prototype to update.' },
279
277
  message: { type: 'string', description: 'The edit instruction.' },
280
278
  coding_agent: { type: 'string', enum: ['bifocal', 'client'], description: 'Who will apply the edit. "bifocal" (default) sends to Bifocal\'s agent. "client" returns the instruction for the calling agent to apply locally.' },
281
279
  },
282
- required: ['project_id', 'prototype_id', 'message'],
280
+ required: ['prototype_id', 'message'],
283
281
  },
284
282
  },
285
283
  {
286
284
  name: 'get_prototypes',
287
- description: 'Get prototypes for a Bifocal project. If prototype_id is provided, returns full detail for that prototype including its sitemap and key capabilities. If omitted, returns a summary list of all prototypes (name, status, published URL, parent prototype, linked solution). Use the full detail form when you need to understand what pages exist or poll for build status.',
285
+ description: 'List all prototypes for a Bifocal project. Returns name, status, published URL, parent prototype (for iteration chains), and linked solution if any. Ordered oldest to newest.',
288
286
  inputSchema: {
289
287
  type: 'object',
290
288
  properties: {
291
- project_id: { type: 'string', description: 'The ID of the project.' },
292
- prototype_id: { type: 'string', description: 'Optional. If provided, returns full detail for this specific prototype.' },
289
+ project_id: { type: 'string', description: 'The ID of the project to fetch prototypes for.' },
293
290
  },
294
291
  required: ['project_id'],
295
292
  },
296
293
  },
294
+ {
295
+ name: 'get_prototype',
296
+ description: 'Get full detail for a specific prototype including its sitemap and key capabilities. Use this when you need to understand what pages exist in a prototype.',
297
+ inputSchema: {
298
+ type: 'object',
299
+ properties: {
300
+ project_id: { type: 'string', description: 'The ID of the project the prototype belongs to.' },
301
+ prototype_id: { type: 'string', description: 'The ID of the prototype to fetch.' },
302
+ },
303
+ required: ['project_id', 'prototype_id'],
304
+ },
305
+ },
297
306
  {
298
307
  name: 'export_prototype',
299
308
  description: 'Download a prototype\'s source code as a ZIP file and save it to the local filesystem. Returns the path where the file was saved.',
@@ -307,6 +316,34 @@ If prototype_id is provided, the zip is uploaded to update an existing prototype
307
316
  required: ['project_id', 'prototype_id'],
308
317
  },
309
318
  },
319
+ {
320
+ name: 'screenshot_prototype',
321
+ description: 'Capture a screenshot of a specific page or UI state in a prototype and render it inline.\n\nBefore calling, make sure you know what path to capture — get valid paths from the prototype\'s sitemap by calling get_prototypes with the prototype_id if you haven\'t already.\n\n**Choosing whether to pass interaction_steps:**\nPass interaction steps when the screen you want to capture requires user interaction to reach — this includes: a specific step in a multi-page flow that can\'t be reached by URL alone, a UI state triggered by a selection or toggle (e.g. a plan option selected, a modal open, a tab active), or any meaningful variation of a screen that differs from its default loaded state. If navigating directly to the path shows the right screen, no steps are needed.\n\nIf you need interaction steps but aren\'t sure what selectors to use, read the relevant page or component source — export the prototype first via export_prototype if you don\'t have the source locally.\n\n**IMPORTANT**: Only call this when you actually need to see something — after an edit, to evaluate a specific screen, or to compare before and after. Don\'t call it speculatively.',
322
+ inputSchema: {
323
+ type: 'object',
324
+ properties: {
325
+ project_id: { type: 'string', description: 'The ID of the project the prototype belongs to.' },
326
+ prototype_id: { type: 'string', description: 'The ID of the prototype to screenshot.' },
327
+ path: { type: 'string', description: 'The URL path to capture (e.g. "/onboarding/step-13"). Must be a real path from the prototype sitemap.' },
328
+ interaction_steps: {
329
+ type: 'array',
330
+ description: 'Optional steps to execute before screenshotting, to reach a specific UI state.',
331
+ items: {
332
+ type: 'object',
333
+ properties: {
334
+ action: { type: 'string', enum: ['click', 'hover', 'waitForSelector', 'waitForTimeout', 'select', 'type'] },
335
+ selector: { type: 'string', description: 'CSS selector for the target element. Required for all actions except waitForTimeout.' },
336
+ ms: { type: 'number', description: 'Milliseconds to wait. Required for waitForTimeout.' },
337
+ value: { type: 'string', description: 'Value to select. Required for select.' },
338
+ text: { type: 'string', description: 'Text to type. Required for type.' },
339
+ },
340
+ required: ['action'],
341
+ },
342
+ },
343
+ },
344
+ required: ['project_id', 'prototype_id', 'path'],
345
+ },
346
+ },
310
347
  ],
311
348
  }));
312
349
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -366,6 +403,58 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
366
403
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
367
404
  }
368
405
  }
406
+ if (name === 'get_import_instructions') {
407
+ const { platform } = args;
408
+ const instructions = {
409
+ claudecode: `⚠️ Do not modify any files in the existing codebase. Read existing files to understand the experience, but never edit them. Create all prototype files in a new separate directory.
410
+
411
+ Bifocal requires a Vite + React single-page app containing only the experience being tested. Follow these steps:
412
+
413
+ 1. Ask the user: "What specific page or flow do you want to test in Bifocal?" Only include what is needed to render that experience. Remove unrelated pages, routes, and components.
414
+
415
+ 2. Ask: "Is this project built with Next.js or Vite?"
416
+
417
+ If Next.js:
418
+ - Convert to a Vite + React single-page app with react-router-dom. Remove all SSR, API routes, getServerSideProps, and getStaticProps.
419
+ - Verify: run npm run build and fix any errors before continuing.
420
+ - Create a ZIP excluding node_modules, .git, dist, and any .env files or credentials.
421
+
422
+ If Vite:
423
+ - Ask: "Does this project use react-router-dom so that each major section has its own URL path?"
424
+ - If no: add react-router-dom with each major section as its own route.
425
+ - Verify: run npm run build and fix any errors before continuing.
426
+ - Create a ZIP excluding node_modules, .git, dist, and any .env files or credentials.
427
+
428
+ 3. Replace any API calls or environment variables with hardcoded fixture data. The prototype must work with no backend and no network requests.
429
+
430
+ 4. Stub out external services — auth, feature flags, analytics, payments, real-time connections — with simple no-op replacements.`,
431
+ lovable: `To export from Lovable: open your project → top-right menu → Export → Download ZIP.
432
+
433
+ Do not manually zip the project files — the Export button handles cleanup automatically.`,
434
+ figmamake: `To export from Figma Make:
435
+ 1. In Figma Make, go to Prototype Settings → GitHub and push your project to a GitHub repo.
436
+ 2. Go to your GitHub repo → click the green Code button → Download ZIP.
437
+
438
+ Note: Some Figma assets may not convert correctly after export. If your build fails or images are missing, check that all asset paths are correct.`,
439
+ other: `⚠️ Do not modify any files in the existing codebase. Read existing files to understand the experience, but never edit them. Create all prototype files in a new separate directory.
440
+
441
+ Bifocal requires a Vite + React single-page app containing only the experience being tested.
442
+
443
+ 1. Ask the user: "What specific page or flow do you want to test in Bifocal?" Only include what is needed to render that experience. Remove unrelated pages, routes, and components.
444
+
445
+ 2. Your project should have package.json, index.html, and src/ at the root. Next.js is not supported — use Vite. npm run build should output to a dist/ or build/ folder.
446
+
447
+ 3. Replace any API calls or environment variables with hardcoded fixture data. The prototype must work with no backend and no network requests.
448
+
449
+ 4. Stub out external services — auth, feature flags, analytics, payments, real-time connections — with simple no-op replacements.
450
+
451
+ 5. Verify: run npm run build and fix any errors before continuing.
452
+
453
+ 6. Create a ZIP excluding node_modules, .git, dist, and any .env files or credentials.`,
454
+ };
455
+ const text = instructions[platform] ?? instructions.other;
456
+ return { content: [{ type: 'text', text }] };
457
+ }
369
458
  if (name === 'update_project') {
370
459
  const { project_id, name: projectName, description } = args;
371
460
  const project = await updateProject(project_id, { name: projectName, description });
@@ -401,7 +490,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
401
490
  const project = await createProject(projectName, description);
402
491
  const response = {
403
492
  ...project,
404
- next_step: "Ask the user if they have a prototype they'd like to import — projects require one to be useful. If yes, call import_prototype the description contains platform-specific instructions for preparing the zip.",
493
+ next_step: "Ask the user if they have a prototype they'd like to import — projects require one to be useful. If yes, call get_import_instructions first to get platform-specific requirements for preparing the zip, then call import_prototype.",
405
494
  };
406
495
  return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
407
496
  }
@@ -436,15 +525,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
436
525
  if (!basePrototype) {
437
526
  throw new Error('No base prototype found for this project. A ready prototype with no parent is required.');
438
527
  }
439
- const generated = await generateSolution(project_id, insight_ids, basePrototype.id, goal, constraints, context_ids);
440
- const { result, timedOut } = await pollUntilReady(() => getSolution(project_id, generated.solution_id), s => s.solution_generation_status === 'completed', 10_000, 3 * 60_000);
441
- if (timedOut) {
442
- return { content: [{ type: 'text', text: JSON.stringify({
443
- solution_id: generated.solution_id,
444
- message: 'Solution is still generating after 3 minutes. Call get_solutions with solution_id to check when it\'s ready.',
445
- }, null, 2) }] };
446
- }
447
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
528
+ const result = await generateSolution(project_id, insight_ids, basePrototype.id, goal, constraints, context_ids);
529
+ return { content: [{ type: 'text', text: JSON.stringify({
530
+ ...result,
531
+ message: 'Solution is being generated. Use get_solutions to check when it appears — typically takes 1-2 minutes.',
532
+ }, null, 2) }] };
448
533
  }
449
534
  if (name === 'create_solution') {
450
535
  const { project_id, title, brief, category, barriers_addressed, interventions, assumptions, open_concerns, insight_ids } = args;
@@ -460,14 +545,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
460
545
  return { content: [{ type: 'text', text: JSON.stringify({ ...result, message: 'Solution updated.' }, null, 2) }] };
461
546
  }
462
547
  if (name === 'get_solutions') {
463
- const { project_id, solution_id } = args;
464
- if (solution_id) {
465
- const solution = await getSolution(project_id, solution_id);
466
- return { content: [{ type: 'text', text: JSON.stringify(solution, null, 2) }] };
467
- }
548
+ const { project_id } = args;
468
549
  const solutions = await getSolutions(project_id);
469
550
  return { content: [{ type: 'text', text: JSON.stringify(solutions, null, 2) }] };
470
551
  }
552
+ if (name === 'get_solution') {
553
+ const { project_id, solution_id } = args;
554
+ const solution = await getSolution(project_id, solution_id);
555
+ return { content: [{ type: 'text', text: JSON.stringify(solution, null, 2) }] };
556
+ }
471
557
  if (name === 'generate_prototype') {
472
558
  const { project_id, solution_id, coding_agent } = args;
473
559
  const result = await generatePrototype(project_id, solution_id, coding_agent);
@@ -482,18 +568,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
482
568
  ],
483
569
  }, null, 2) }] };
484
570
  }
485
- const bifocalResult = result;
486
- const { result: prototype, timedOut } = await pollUntilReady(() => getPrototype(project_id, bifocalResult.prototype_id), p => p.status === 'ready', 15_000, 10 * 60_000);
487
- if (timedOut) {
488
- return { content: [{ type: 'text', text: JSON.stringify({
489
- prototype_id: bifocalResult.prototype_id,
490
- message: 'Prototype is still building after 10 minutes. Call get_prototypes with prototype_id to check when it\'s ready.',
491
- }, null, 2) }] };
492
- }
493
- return { content: [{ type: 'text', text: JSON.stringify(prototype, null, 2) }] };
571
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
494
572
  }
495
573
  if (name === 'update_prototype') {
496
- const { project_id, prototype_id, message, coding_agent } = args;
574
+ const { prototype_id, message, coding_agent } = args;
497
575
  if (coding_agent === 'client') {
498
576
  return { content: [{ type: 'text', text: JSON.stringify({
499
577
  prototype_id,
@@ -505,25 +583,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
505
583
  ],
506
584
  }, null, 2) }] };
507
585
  }
508
- await updatePrototype(prototype_id, message);
509
- const { result: prototype, timedOut } = await pollUntilReady(() => getPrototype(project_id, prototype_id), p => p.status === 'ready', 15_000, 10 * 60_000);
510
- if (timedOut) {
511
- return { content: [{ type: 'text', text: JSON.stringify({
512
- prototype_id,
513
- message: 'Prototype is still building after 10 minutes. Call get_prototypes with prototype_id to check when it\'s ready.',
514
- }, null, 2) }] };
515
- }
516
- return { content: [{ type: 'text', text: JSON.stringify(prototype, null, 2) }] };
586
+ const result = await updatePrototype(prototype_id, message);
587
+ return { content: [{ type: 'text', text: JSON.stringify({
588
+ ...result,
589
+ message: 'Edit queued. Call get_prototype to poll until status returns to "ready".',
590
+ }, null, 2) }] };
517
591
  }
518
592
  if (name === 'get_prototypes') {
519
- const { project_id, prototype_id } = args;
520
- if (prototype_id) {
521
- const prototype = await getPrototype(project_id, prototype_id);
522
- return { content: [{ type: 'text', text: JSON.stringify(prototype, null, 2) }] };
523
- }
593
+ const { project_id } = args;
524
594
  const prototypes = await getPrototypes(project_id);
525
595
  return { content: [{ type: 'text', text: JSON.stringify(prototypes, null, 2) }] };
526
596
  }
597
+ if (name === 'get_prototype') {
598
+ const { project_id, prototype_id } = args;
599
+ const prototype = await getPrototype(project_id, prototype_id);
600
+ return { content: [{ type: 'text', text: JSON.stringify(prototype, null, 2) }] };
601
+ }
527
602
  if (name === 'export_prototype') {
528
603
  const { project_id, prototype_id, output_dir } = args;
529
604
  const { downloadUrl, fileName } = await exportPrototype(project_id, prototype_id);
@@ -536,6 +611,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
536
611
  await writeFile(filePath, buffer);
537
612
  return { content: [{ type: 'text', text: `Saved to ${filePath}` }] };
538
613
  }
614
+ if (name === 'screenshot_prototype') {
615
+ const { prototype_id, path, interaction_steps } = args;
616
+ const { screenshot, mime_type } = await screenshotPrototype(prototype_id, path, interaction_steps);
617
+ return { content: [{ type: 'image', data: screenshot, mimeType: mime_type }] };
618
+ }
539
619
  return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
540
620
  }
541
621
  catch (error) {
@@ -545,115 +625,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
545
625
  };
546
626
  }
547
627
  });
548
- const PRIMER = `# Bifocal — What It Is and How to Use It
549
-
550
- ## What is Bifocal?
551
- Bifocal is a product discovery platform. It takes raw user research (interview transcripts, usability session notes, feedback documents) and turns it into structured insights, then uses those insights to generate and iterate on interactive prototypes — all without writing code manually.
552
-
553
- The typical use case: a product team has run user research sessions. They upload the raw feedback into Bifocal, which analyzes it and extracts structured insights (barriers, goals, mention counts). They then use those insights to generate solution hypotheses and working prototypes to test with users.
554
-
555
- ## Core Concepts
556
-
557
- **Project**
558
- The top-level container. Represents a product area or research initiative (e.g. "Onboarding Flow Redesign"). Holds all the feedback, insights, solutions, and prototypes for that initiative.
559
-
560
- **Feedback**
561
- Raw user research uploaded to a project — interview transcripts, session notes, usability findings. Bifocal processes this into structured insights.
562
-
563
- **Insight**
564
- A structured finding extracted from feedback. Each insight has a title, description, category, user goal, specific barriers, affected pages, and a mention count (how many feedback sessions surfaced this issue). Insights are ordered by mention count — higher count = more critical.
565
-
566
- **Solution**
567
- A hypothesis for how to address one or more insights. Contains a set of interventions — each intervention targets a specific barrier and describes exact UI/UX changes to make (which pages to modify, what to change, why). Solutions are generated from insights but can also be created manually.
568
-
569
- **Prototype**
570
- A working, deployed web app (Vite + React) built from a solution. Accessible at a public URL. Prototypes are either generated by Bifocal's coding agent from a solution spec, or built locally and imported. Each prototype forks from a base prototype and only applies the changes described in its solution.
571
-
572
- **Context**
573
- Reusable reference material attached to a project — design tokens, component specs, brand guidelines, page layouts. Contexts are injected into solution and prototype generation to ensure consistency.
574
-
575
- ## Data Model
576
- \`\`\`
577
- Project
578
- └── Feedback (raw research)
579
- └── Insights (structured findings, ordered by mention_count)
580
- └── Contexts (design system, brand guidelines)
581
- └── Solutions (hypotheses built from insights)
582
- └── Prototype (deployed web app)
583
- \`\`\`
584
-
585
- ## Standard Workflow
586
-
587
- 1. **Orient** — call \`list_projects\` to find the project, then \`get_insights\` to understand what the research says. Insights with high mention counts are the most critical to address.
588
-
589
- 2. **Plan** — select 1–3 insights to address. Consider the user's goal and any constraints. Optionally call \`get_contexts\` to understand design constraints.
590
-
591
- 3. **Generate a solution** — call \`generate_solution\` with the selected insight IDs, a goal, and any constraints. Blocks until ready and returns the full solution detail — typically 1–2 minutes.
592
-
593
- 4. **Build a prototype** — call \`generate_prototype\` with the solution ID. Blocks until the prototype is ready and returns the full detail including \`published_url\` — typically 3–5 minutes.
594
-
595
- 5. **Iterate** — use \`update_prototype\` to refine a prototype with specific edit instructions, or generate a new solution from different insights. Use \`add_feedback\` to log what you learn from testing.
596
-
597
- ## Tool Reference by Category
598
-
599
- **Finding your project**
600
- - \`list_projects\` — start here, always
601
-
602
- **Understanding the research**
603
- - \`get_insights\` — structured findings ordered by mention count; read these before generating anything
604
- - \`get_quotes\` — raw supporting quotes for a specific insight; use when you need more evidence
605
-
606
- **Design system / constraints**
607
- - \`get_contexts\` — read design tokens, component specs, brand guidelines for the project
608
- - \`create_context\` — add new context (design system, brand guidelines, etc.)
609
-
610
- **Solutions**
611
- - \`generate_solution\` — async; requires insight IDs, optional goal + constraints; always elicit these from the user first
612
- - \`get_solutions\` — list all solutions, or pass solution_id for full detail including interventions and page changes
613
- - \`create_solution\` — manually specify a solution without using the generator
614
- - \`update_solution\` — modify an existing solution
615
-
616
- **Prototypes**
617
- - \`generate_prototype\` — async; builds from a solution; use \`coding_agent: "bifocal"\` (default) or \`"client"\` (to build locally)
618
- - \`get_prototypes\` — list all prototypes, or pass prototype_id for full detail including sitemap; use to poll for ready status
619
- - \`update_prototype\` — async edit instruction to an existing prototype; use for targeted refinements
620
- - \`export_prototype\` — download source as ZIP for local editing
621
- - \`import_prototype\` — upload a locally built or edited ZIP; includes platform-specific instructions
622
-
623
- **Feedback**
624
- - \`add_feedback\` — add text or PDF research to a project, linked to a prototype
625
-
626
- ## Key Behaviors to Know
627
-
628
- - **Blocking operations**: \`generate_solution\`, \`generate_prototype\`, and \`update_prototype\` block until the result is ready and return the full detail directly — no polling needed. If they time out, they return a message with the ID so you can check status manually via \`get_solutions\` or \`get_prototypes\`.
629
-
630
- - **Base prototype**: Every project has a base prototype that all solution prototypes fork from. The base prototype ID is resolved automatically by \`generate_prototype\` — you do not need to specify it.
631
-
632
- - **Client vs Bifocal coding agent**: \`generate_prototype\` and \`update_prototype\` both support \`coding_agent: "client"\`. Use this when you want to build or edit the prototype locally (exports the spec + base code, you implement and import back). Use \`"bifocal"\` (default) when you want Bifocal to build it automatically.
633
-
634
- - **Solution elicitation**: Before calling \`generate_solution\`, always show the user the available insights and ask which to prioritize, what the goal is, and whether there are constraints. The tool description spells this out explicitly.
635
-
636
- - **Prototype forking**: Prototypes are built from a base, so each one only contains the changes from its solution — not the entire app. When comparing prototypes, they share the same baseline.
637
- `;
638
- server.setRequestHandler(ListResourcesRequestSchema, async () => ({
639
- resources: [
640
- {
641
- uri: 'bifocal://primer',
642
- name: 'Bifocal Primer',
643
- description: 'Start here — overview of what Bifocal is, core concepts, data model, standard workflow, and tool reference. Read this before using any tools.',
644
- mimeType: 'text/markdown',
645
- },
646
- ],
647
- }));
648
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
649
- const { uri } = request.params;
650
- if (uri === 'bifocal://primer') {
651
- return {
652
- contents: [{ uri, mimeType: 'text/markdown', text: PRIMER }],
653
- };
654
- }
655
- throw new Error(`Unknown resource: ${uri}`);
656
- });
657
628
  async function main() {
658
629
  const transport = new StdioServerTransport();
659
630
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bifocal/mcp",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Bifocal MCP server — access projects, insights, and prototypes from Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",