@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.
- package/dist/bifocalClient.js +12 -0
- package/dist/index.js +164 -193
- package/package.json +1 -1
package/dist/bifocalClient.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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:
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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\
|
|
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\
|
|
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: ['
|
|
280
|
+
required: ['prototype_id', 'message'],
|
|
283
281
|
},
|
|
284
282
|
},
|
|
285
283
|
{
|
|
286
284
|
name: 'get_prototypes',
|
|
287
|
-
description: '
|
|
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
|
|
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
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
|
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);
|