@bifocal/mcp 0.1.5 → 0.1.7
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/index.js +192 -130
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,16 +1,43 @@
|
|
|
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, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
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';
|
|
6
6
|
import { writeFile } from 'fs/promises';
|
|
7
7
|
import { join } from 'path';
|
|
8
|
-
|
|
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: {} } });
|
|
9
20
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
10
21
|
tools: [
|
|
11
22
|
{
|
|
12
23
|
name: 'import_prototype',
|
|
13
|
-
description:
|
|
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.`,
|
|
14
41
|
inputSchema: {
|
|
15
42
|
type: 'object',
|
|
16
43
|
properties: {
|
|
@@ -36,21 +63,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
36
63
|
required: ['project_id', 'prototype_id', 'source'],
|
|
37
64
|
},
|
|
38
65
|
},
|
|
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
|
-
},
|
|
54
66
|
{
|
|
55
67
|
name: 'create_project',
|
|
56
68
|
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.',
|
|
@@ -106,7 +118,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
106
118
|
},
|
|
107
119
|
{
|
|
108
120
|
name: 'create_context',
|
|
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.',
|
|
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.',
|
|
110
122
|
inputSchema: {
|
|
111
123
|
type: 'object',
|
|
112
124
|
properties: {
|
|
@@ -132,7 +144,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
132
144
|
},
|
|
133
145
|
{
|
|
134
146
|
name: 'generate_solution',
|
|
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.
|
|
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.',
|
|
136
148
|
inputSchema: {
|
|
137
149
|
type: 'object',
|
|
138
150
|
properties: {
|
|
@@ -147,7 +159,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
147
159
|
},
|
|
148
160
|
{
|
|
149
161
|
name: 'create_solution',
|
|
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
|
|
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>',
|
|
151
163
|
inputSchema: {
|
|
152
164
|
type: 'object',
|
|
153
165
|
properties: {
|
|
@@ -190,7 +202,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
190
202
|
},
|
|
191
203
|
{
|
|
192
204
|
name: 'update_solution',
|
|
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
|
|
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>',
|
|
194
206
|
inputSchema: {
|
|
195
207
|
type: 'object',
|
|
196
208
|
properties: {
|
|
@@ -233,30 +245,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
233
245
|
},
|
|
234
246
|
{
|
|
235
247
|
name: 'get_solutions',
|
|
236
|
-
description: 'Get
|
|
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).',
|
|
237
249
|
inputSchema: {
|
|
238
250
|
type: 'object',
|
|
239
251
|
properties: {
|
|
240
|
-
project_id: { type: 'string', description: 'The ID of the project
|
|
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.' },
|
|
241
254
|
},
|
|
242
255
|
required: ['project_id'],
|
|
243
256
|
},
|
|
244
257
|
},
|
|
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
|
-
},
|
|
257
258
|
{
|
|
258
259
|
name: 'generate_prototype',
|
|
259
|
-
description: 'Generate a prototype from a solution.\n\nIf coding_agent is "bifocal" (default): queues
|
|
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.',
|
|
260
261
|
inputSchema: {
|
|
261
262
|
type: 'object',
|
|
262
263
|
properties: {
|
|
@@ -269,40 +270,30 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
269
270
|
},
|
|
270
271
|
{
|
|
271
272
|
name: 'update_prototype',
|
|
272
|
-
description: 'Edit an existing prototype.\n\nIf coding_agent is "bifocal" (default): sends the edit instruction to Bifocal\'s coding agent
|
|
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.',
|
|
273
274
|
inputSchema: {
|
|
274
275
|
type: 'object',
|
|
275
276
|
properties: {
|
|
277
|
+
project_id: { type: 'string', description: 'The ID of the project the prototype belongs to.' },
|
|
276
278
|
prototype_id: { type: 'string', description: 'The ID of the prototype to update.' },
|
|
277
279
|
message: { type: 'string', description: 'The edit instruction.' },
|
|
278
280
|
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.' },
|
|
279
281
|
},
|
|
280
|
-
required: ['prototype_id', 'message'],
|
|
282
|
+
required: ['project_id', 'prototype_id', 'message'],
|
|
281
283
|
},
|
|
282
284
|
},
|
|
283
285
|
{
|
|
284
286
|
name: 'get_prototypes',
|
|
285
|
-
description: '
|
|
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.',
|
|
286
288
|
inputSchema: {
|
|
287
289
|
type: 'object',
|
|
288
290
|
properties: {
|
|
289
|
-
project_id: { type: 'string', description: 'The ID of the project
|
|
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.' },
|
|
290
293
|
},
|
|
291
294
|
required: ['project_id'],
|
|
292
295
|
},
|
|
293
296
|
},
|
|
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
|
-
},
|
|
306
297
|
{
|
|
307
298
|
name: 'export_prototype',
|
|
308
299
|
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.',
|
|
@@ -375,58 +366,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
375
366
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
376
367
|
}
|
|
377
368
|
}
|
|
378
|
-
if (name === 'get_import_instructions') {
|
|
379
|
-
const { platform } = args;
|
|
380
|
-
const instructions = {
|
|
381
|
-
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.
|
|
382
|
-
|
|
383
|
-
Bifocal requires a Vite + React single-page app containing only the experience being tested. Follow these steps:
|
|
384
|
-
|
|
385
|
-
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.
|
|
386
|
-
|
|
387
|
-
2. Ask: "Is this project built with Next.js or Vite?"
|
|
388
|
-
|
|
389
|
-
If Next.js:
|
|
390
|
-
- Convert to a Vite + React single-page app with react-router-dom. Remove all SSR, API routes, getServerSideProps, and getStaticProps.
|
|
391
|
-
- Verify: run npm run build and fix any errors before continuing.
|
|
392
|
-
- Create a ZIP excluding node_modules, .git, dist, and any .env files or credentials.
|
|
393
|
-
|
|
394
|
-
If Vite:
|
|
395
|
-
- Ask: "Does this project use react-router-dom so that each major section has its own URL path?"
|
|
396
|
-
- If no: add react-router-dom with each major section as its own route.
|
|
397
|
-
- Verify: run npm run build and fix any errors before continuing.
|
|
398
|
-
- Create a ZIP excluding node_modules, .git, dist, and any .env files or credentials.
|
|
399
|
-
|
|
400
|
-
3. Replace any API calls or environment variables with hardcoded fixture data. The prototype must work with no backend and no network requests.
|
|
401
|
-
|
|
402
|
-
4. Stub out external services — auth, feature flags, analytics, payments, real-time connections — with simple no-op replacements.`,
|
|
403
|
-
lovable: `To export from Lovable: open your project → top-right menu → Export → Download ZIP.
|
|
404
|
-
|
|
405
|
-
Do not manually zip the project files — the Export button handles cleanup automatically.`,
|
|
406
|
-
figmamake: `To export from Figma Make:
|
|
407
|
-
1. In Figma Make, go to Prototype Settings → GitHub and push your project to a GitHub repo.
|
|
408
|
-
2. Go to your GitHub repo → click the green Code button → Download ZIP.
|
|
409
|
-
|
|
410
|
-
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.`,
|
|
411
|
-
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.
|
|
412
|
-
|
|
413
|
-
Bifocal requires a Vite + React single-page app containing only the experience being tested.
|
|
414
|
-
|
|
415
|
-
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.
|
|
416
|
-
|
|
417
|
-
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.
|
|
418
|
-
|
|
419
|
-
3. Replace any API calls or environment variables with hardcoded fixture data. The prototype must work with no backend and no network requests.
|
|
420
|
-
|
|
421
|
-
4. Stub out external services — auth, feature flags, analytics, payments, real-time connections — with simple no-op replacements.
|
|
422
|
-
|
|
423
|
-
5. Verify: run npm run build and fix any errors before continuing.
|
|
424
|
-
|
|
425
|
-
6. Create a ZIP excluding node_modules, .git, dist, and any .env files or credentials.`,
|
|
426
|
-
};
|
|
427
|
-
const text = instructions[platform] ?? instructions.other;
|
|
428
|
-
return { content: [{ type: 'text', text }] };
|
|
429
|
-
}
|
|
430
369
|
if (name === 'update_project') {
|
|
431
370
|
const { project_id, name: projectName, description } = args;
|
|
432
371
|
const project = await updateProject(project_id, { name: projectName, description });
|
|
@@ -462,7 +401,7 @@ Bifocal requires a Vite + React single-page app containing only the experience b
|
|
|
462
401
|
const project = await createProject(projectName, description);
|
|
463
402
|
const response = {
|
|
464
403
|
...project,
|
|
465
|
-
next_step: "Ask the user if they have a prototype they'd like to import — projects require one to be useful. If yes, call
|
|
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.",
|
|
466
405
|
};
|
|
467
406
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
|
468
407
|
}
|
|
@@ -497,11 +436,15 @@ Bifocal requires a Vite + React single-page app containing only the experience b
|
|
|
497
436
|
if (!basePrototype) {
|
|
498
437
|
throw new Error('No base prototype found for this project. A ready prototype with no parent is required.');
|
|
499
438
|
}
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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) }] };
|
|
505
448
|
}
|
|
506
449
|
if (name === 'create_solution') {
|
|
507
450
|
const { project_id, title, brief, category, barriers_addressed, interventions, assumptions, open_concerns, insight_ids } = args;
|
|
@@ -517,15 +460,14 @@ Bifocal requires a Vite + React single-page app containing only the experience b
|
|
|
517
460
|
return { content: [{ type: 'text', text: JSON.stringify({ ...result, message: 'Solution updated.' }, null, 2) }] };
|
|
518
461
|
}
|
|
519
462
|
if (name === 'get_solutions') {
|
|
520
|
-
const { project_id } = args;
|
|
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
|
+
}
|
|
521
468
|
const solutions = await getSolutions(project_id);
|
|
522
469
|
return { content: [{ type: 'text', text: JSON.stringify(solutions, null, 2) }] };
|
|
523
470
|
}
|
|
524
|
-
if (name === 'get_solution') {
|
|
525
|
-
const { project_id, solution_id } = args;
|
|
526
|
-
const solution = await getSolution(project_id, solution_id);
|
|
527
|
-
return { content: [{ type: 'text', text: JSON.stringify(solution, null, 2) }] };
|
|
528
|
-
}
|
|
529
471
|
if (name === 'generate_prototype') {
|
|
530
472
|
const { project_id, solution_id, coding_agent } = args;
|
|
531
473
|
const result = await generatePrototype(project_id, solution_id, coding_agent);
|
|
@@ -540,10 +482,18 @@ Bifocal requires a Vite + React single-page app containing only the experience b
|
|
|
540
482
|
],
|
|
541
483
|
}, null, 2) }] };
|
|
542
484
|
}
|
|
543
|
-
|
|
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) }] };
|
|
544
494
|
}
|
|
545
495
|
if (name === 'update_prototype') {
|
|
546
|
-
const { prototype_id, message, coding_agent } = args;
|
|
496
|
+
const { project_id, prototype_id, message, coding_agent } = args;
|
|
547
497
|
if (coding_agent === 'client') {
|
|
548
498
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
549
499
|
prototype_id,
|
|
@@ -555,22 +505,25 @@ Bifocal requires a Vite + React single-page app containing only the experience b
|
|
|
555
505
|
],
|
|
556
506
|
}, null, 2) }] };
|
|
557
507
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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) }] };
|
|
563
517
|
}
|
|
564
518
|
if (name === 'get_prototypes') {
|
|
565
|
-
const { project_id } = args;
|
|
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
|
+
}
|
|
566
524
|
const prototypes = await getPrototypes(project_id);
|
|
567
525
|
return { content: [{ type: 'text', text: JSON.stringify(prototypes, null, 2) }] };
|
|
568
526
|
}
|
|
569
|
-
if (name === 'get_prototype') {
|
|
570
|
-
const { project_id, prototype_id } = args;
|
|
571
|
-
const prototype = await getPrototype(project_id, prototype_id);
|
|
572
|
-
return { content: [{ type: 'text', text: JSON.stringify(prototype, null, 2) }] };
|
|
573
|
-
}
|
|
574
527
|
if (name === 'export_prototype') {
|
|
575
528
|
const { project_id, prototype_id, output_dir } = args;
|
|
576
529
|
const { downloadUrl, fileName } = await exportPrototype(project_id, prototype_id);
|
|
@@ -592,6 +545,115 @@ Bifocal requires a Vite + React single-page app containing only the experience b
|
|
|
592
545
|
};
|
|
593
546
|
}
|
|
594
547
|
});
|
|
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
|
+
});
|
|
595
657
|
async function main() {
|
|
596
658
|
const transport = new StdioServerTransport();
|
|
597
659
|
await server.connect(transport);
|