@bifocal/mcp 0.1.4 → 0.1.6
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 +58 -5
- package/dist/index.js +312 -16
- package/package.json +1 -1
package/dist/bifocalClient.js
CHANGED
|
@@ -17,11 +17,39 @@ async function get(path) {
|
|
|
17
17
|
}
|
|
18
18
|
return response.json();
|
|
19
19
|
}
|
|
20
|
-
export async function
|
|
20
|
+
export async function getContexts(projectId) {
|
|
21
|
+
const data = await get(`/api/projects/${projectId}/contexts`);
|
|
22
|
+
return data.contexts;
|
|
23
|
+
}
|
|
24
|
+
export async function createSolution(projectId, title, brief, category, barriers_addressed, interventions, assumptions, open_concerns, insight_ids) {
|
|
25
|
+
const response = await fetch(`${API_URL}/api/projects/${projectId}/solutions`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: headers(),
|
|
28
|
+
body: JSON.stringify({ title, brief, category, barriers_addressed, interventions, assumptions, open_concerns, insight_ids }),
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const error = await response.json().catch(() => ({}));
|
|
32
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
33
|
+
}
|
|
34
|
+
return response.json();
|
|
35
|
+
}
|
|
36
|
+
export async function createContext(projectId, name, type, content, description) {
|
|
37
|
+
const response = await fetch(`${API_URL}/api/projects/${projectId}/contexts`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: headers(),
|
|
40
|
+
body: JSON.stringify({ name, type, content, description }),
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const error = await response.json().catch(() => ({}));
|
|
44
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
45
|
+
}
|
|
46
|
+
return response.json();
|
|
47
|
+
}
|
|
48
|
+
export async function generateSolution(projectId, insightIds, prototypeId, goal, constraints, contextIds) {
|
|
21
49
|
const response = await fetch(`${API_URL}/api/projects/${projectId}/solutions/generate`, {
|
|
22
50
|
method: 'POST',
|
|
23
51
|
headers: headers(),
|
|
24
|
-
body: JSON.stringify({ insightIds, prototypeId, goal, constraints }),
|
|
52
|
+
body: JSON.stringify({ insightIds, prototypeId, goal, constraints, contextIds }),
|
|
25
53
|
});
|
|
26
54
|
if (!response.ok) {
|
|
27
55
|
const error = await response.json().catch(() => ({}));
|
|
@@ -83,10 +111,35 @@ export async function getPrototype(projectId, prototypeId) {
|
|
|
83
111
|
export async function exportPrototype(projectId, prototypeId) {
|
|
84
112
|
return get(`/api/projects/${projectId}/prototypes/${prototypeId}/download-zip`);
|
|
85
113
|
}
|
|
86
|
-
export async function
|
|
114
|
+
export async function updateSolution(projectId, solutionId, updates) {
|
|
115
|
+
const response = await fetch(`${API_URL}/api/projects/${projectId}/solutions/${solutionId}`, {
|
|
116
|
+
method: 'PATCH',
|
|
117
|
+
headers: headers(),
|
|
118
|
+
body: JSON.stringify(updates),
|
|
119
|
+
});
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
const error = await response.json().catch(() => ({}));
|
|
122
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
123
|
+
}
|
|
124
|
+
return response.json();
|
|
125
|
+
}
|
|
126
|
+
export async function updatePrototype(prototypeId, message) {
|
|
127
|
+
const response = await fetch(`${API_URL}/api/prototypes/chat`, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: headers(),
|
|
130
|
+
body: JSON.stringify({ prototypeId, message }),
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
const error = await response.json().catch(() => ({}));
|
|
134
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
135
|
+
}
|
|
136
|
+
return response.json();
|
|
137
|
+
}
|
|
138
|
+
export async function generatePrototype(projectId, solutionId, codingAgent) {
|
|
87
139
|
const response = await fetch(`${API_URL}/api/projects/${projectId}/solutions/${solutionId}/generate-prototype`, {
|
|
88
140
|
method: 'POST',
|
|
89
141
|
headers: headers(),
|
|
142
|
+
body: codingAgent ? JSON.stringify({ coding_agent: codingAgent }) : undefined,
|
|
90
143
|
});
|
|
91
144
|
if (!response.ok) {
|
|
92
145
|
const error = await response.json().catch(() => ({}));
|
|
@@ -94,11 +147,11 @@ export async function generatePrototype(projectId, solutionId) {
|
|
|
94
147
|
}
|
|
95
148
|
return response.json();
|
|
96
149
|
}
|
|
97
|
-
export async function importPrototypeUploadUrl(projectId, prototypeName,
|
|
150
|
+
export async function importPrototypeUploadUrl(projectId, filename, prototypeName, prototypeId) {
|
|
98
151
|
const response = await fetch(`${API_URL}/api/projects/${projectId}/prototypes/import/upload-url`, {
|
|
99
152
|
method: 'POST',
|
|
100
153
|
headers: headers(),
|
|
101
|
-
body: JSON.stringify({ prototype_name: prototypeName, filename }),
|
|
154
|
+
body: JSON.stringify({ prototype_name: prototypeName, filename, prototype_id: prototypeId }),
|
|
102
155
|
});
|
|
103
156
|
if (!response.ok) {
|
|
104
157
|
const error = await response.json().catch(() => ({}));
|
package/dist/index.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
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';
|
|
5
|
-
import { createProject, updateProject, listProjects, importPrototypeUploadUrl, importPrototypeConfirm, addFeedbackText, generateSolution, getInsights, getQuotes, getSolutions, getSolution, getPrototypes, getPrototype, exportPrototype, generatePrototype } from './bifocalClient.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';
|
|
6
6
|
import { writeFile } from 'fs/promises';
|
|
7
7
|
import { join } from 'path';
|
|
8
|
-
const server = new Server({ name: 'bifocal', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
8
|
+
const server = new Server({ name: 'bifocal', version: '0.1.0' }, { capabilities: { tools: {}, resources: {} } });
|
|
9
9
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
10
10
|
tools: [
|
|
11
11
|
{
|
|
12
12
|
name: 'import_prototype',
|
|
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.',
|
|
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.',
|
|
14
14
|
inputSchema: {
|
|
15
15
|
type: 'object',
|
|
16
16
|
properties: {
|
|
17
17
|
project_id: { type: 'string', description: 'The ID of the project to import into.' },
|
|
18
|
-
prototype_name: { type: 'string', description: 'A name for the prototype.' },
|
|
18
|
+
prototype_name: { type: 'string', description: 'A name for the prototype. Required when creating a new prototype (no prototype_id).' },
|
|
19
19
|
zip_path: { type: 'string', description: 'Absolute path to the zip file on the local filesystem.' },
|
|
20
|
+
prototype_id: { type: 'string', description: 'If provided, updates this existing prototype instead of creating a new one. Use this after building or editing a prototype locally.' },
|
|
20
21
|
},
|
|
21
|
-
required: ['project_id', '
|
|
22
|
+
required: ['project_id', 'zip_path'],
|
|
22
23
|
},
|
|
23
24
|
},
|
|
24
25
|
{
|
|
@@ -103,19 +104,131 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
103
104
|
required: ['project_id', 'insight_id'],
|
|
104
105
|
},
|
|
105
106
|
},
|
|
107
|
+
{
|
|
108
|
+
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.',
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
project_id: { type: 'string', description: 'The ID of the project.' },
|
|
114
|
+
name: { type: 'string', description: 'A short name for the context (e.g. "Brand Guidelines", "Design System").' },
|
|
115
|
+
type: { type: 'string', description: 'The type of context (e.g. "product", "design", "brand", "research", "other").' },
|
|
116
|
+
content: { type: 'string', description: 'The full text content of the context.' },
|
|
117
|
+
description: { type: 'string', description: 'Optional short description of what this context contains.' },
|
|
118
|
+
},
|
|
119
|
+
required: ['project_id', 'name', 'type', 'content'],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'get_contexts',
|
|
124
|
+
description: 'Get all contexts for a Bifocal project. Contexts contain product, design, or other reference material that can be attached to a solution to guide generation. Call this before generate_solution if you want to include context.',
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
project_id: { type: 'string', description: 'The ID of the project.' },
|
|
129
|
+
},
|
|
130
|
+
required: ['project_id'],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
106
133
|
{
|
|
107
134
|
name: 'generate_solution',
|
|
108
|
-
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. Only call this tool once you have that input. This is async — use get_solutions to check when the solution appears.',
|
|
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.',
|
|
109
136
|
inputSchema: {
|
|
110
137
|
type: 'object',
|
|
111
138
|
properties: {
|
|
112
139
|
project_id: { type: 'string', description: 'The ID of the project.' },
|
|
113
140
|
insight_ids: { type: 'array', items: { type: 'string' }, description: 'IDs of insights to base the solution on. Use get_insights to find them.' },
|
|
114
|
-
prototype_id: { type: 'string', description: 'The ID of the prototype to use as context for the solution.' },
|
|
115
141
|
goal: { type: 'string', description: 'Optional goal or focus for the solution.' },
|
|
116
142
|
constraints: { type: 'string', description: 'Optional constraints to consider.' },
|
|
143
|
+
context_ids: { type: 'array', items: { type: 'string' }, description: 'Optional IDs of contexts to attach. Use get_contexts to find them.' },
|
|
144
|
+
},
|
|
145
|
+
required: ['project_id', 'insight_ids'],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
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 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.',
|
|
151
|
+
inputSchema: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
project_id: { type: 'string', description: 'The ID of the project.' },
|
|
155
|
+
title: { type: 'string', description: 'Concise name for the solution (e.g. "Condensed Flow with Unified Plan Builder").' },
|
|
156
|
+
brief: { type: 'string', description: '1-2 sentence summary of what the solution does and why.' },
|
|
157
|
+
category: { type: 'string', enum: ['feature', 'design', 'copy', 'content', 'flow', 'pricing', 'simplification', 'other'], description: 'Category that best describes the type of change.' },
|
|
158
|
+
barriers_addressed: { type: 'array', items: { type: 'string' }, description: 'List of user barriers or pain points this solution directly addresses.' },
|
|
159
|
+
interventions: {
|
|
160
|
+
type: 'array',
|
|
161
|
+
description: 'One or more interventions, each addressing a specific barrier. Each intervention contains a set of concrete UI/UX changes to implement.',
|
|
162
|
+
items: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
barrier: { type: 'string', description: 'The specific barrier or problem this intervention targets.' },
|
|
166
|
+
changes: {
|
|
167
|
+
type: 'array',
|
|
168
|
+
description: 'Concrete changes to implement for this barrier.',
|
|
169
|
+
items: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: {
|
|
172
|
+
approach: { type: 'string', description: 'Short name/headline for this change (e.g. "Add frequency price cards to step 13").' },
|
|
173
|
+
description: { type: 'string', description: 'Detailed description of what to build or change, including layout, copy, interactions, and any edge cases. Be specific enough for a coding agent to implement it.' },
|
|
174
|
+
pages_to_modify: { type: 'array', items: { type: 'string' }, description: 'Existing page route paths to modify (e.g. ["/onboarding/step-13"]). Empty array if none.' },
|
|
175
|
+
pages_to_create: { type: 'array', items: { type: 'string' }, description: 'New page route paths to create (e.g. ["/menu-preview"]). Empty array if none.' },
|
|
176
|
+
},
|
|
177
|
+
required: ['approach', 'description', 'pages_to_modify', 'pages_to_create'],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
required: ['barrier', 'changes'],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
assumptions: { type: 'array', items: { type: 'string' }, description: 'Assumptions this solution makes about the product, users, or implementation. Use an empty array if none.' },
|
|
185
|
+
open_concerns: { type: 'array', items: { type: 'string' }, description: 'Open questions, risks, or things to validate. Use an empty array if none.' },
|
|
186
|
+
insight_ids: { type: 'array', items: { type: 'string' }, description: 'IDs of insights this solution addresses. Use get_insights to find them.' },
|
|
117
187
|
},
|
|
118
|
-
required: ['project_id', '
|
|
188
|
+
required: ['project_id', 'title', 'brief', 'category', 'barriers_addressed', 'interventions', 'assumptions', 'open_concerns', 'insight_ids'],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
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 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.',
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
project_id: { type: 'string', description: 'The ID of the project.' },
|
|
198
|
+
solution_id: { type: 'string', description: 'The ID of the solution to update.' },
|
|
199
|
+
title: { type: 'string', description: 'Updated title for the solution.' },
|
|
200
|
+
brief: { type: 'string', description: 'Updated 1-2 sentence summary.' },
|
|
201
|
+
category: { type: 'string', enum: ['feature', 'design', 'copy', 'content', 'flow', 'pricing', 'simplification', 'other'], description: 'Updated category.' },
|
|
202
|
+
barriers_addressed: { type: 'array', items: { type: 'string' }, description: 'Full replacement list of barriers addressed.' },
|
|
203
|
+
interventions: {
|
|
204
|
+
type: 'array',
|
|
205
|
+
description: 'Full replacement list of interventions. If provided, selected_changes will be re-derived automatically.',
|
|
206
|
+
items: {
|
|
207
|
+
type: 'object',
|
|
208
|
+
properties: {
|
|
209
|
+
barrier: { type: 'string', description: 'The specific barrier this intervention targets.' },
|
|
210
|
+
changes: {
|
|
211
|
+
type: 'array',
|
|
212
|
+
items: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: {
|
|
215
|
+
approach: { type: 'string' },
|
|
216
|
+
description: { type: 'string' },
|
|
217
|
+
pages_to_modify: { type: 'array', items: { type: 'string' } },
|
|
218
|
+
pages_to_create: { type: 'array', items: { type: 'string' } },
|
|
219
|
+
},
|
|
220
|
+
required: ['approach', 'description', 'pages_to_modify', 'pages_to_create'],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
required: ['barrier', 'changes'],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
assumptions: { type: 'array', items: { type: 'string' }, description: 'Full replacement list of assumptions.' },
|
|
228
|
+
open_concerns: { type: 'array', items: { type: 'string' }, description: 'Full replacement list of open concerns.' },
|
|
229
|
+
insight_ids: { type: 'array', items: { type: 'string' }, description: 'Full replacement list of insight IDs. Replaces all existing links.' },
|
|
230
|
+
},
|
|
231
|
+
required: ['project_id', 'solution_id'],
|
|
119
232
|
},
|
|
120
233
|
},
|
|
121
234
|
{
|
|
@@ -143,16 +256,30 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
143
256
|
},
|
|
144
257
|
{
|
|
145
258
|
name: 'generate_prototype',
|
|
146
|
-
description: 'Generate a prototype from a solution
|
|
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.',
|
|
147
260
|
inputSchema: {
|
|
148
261
|
type: 'object',
|
|
149
262
|
properties: {
|
|
150
263
|
project_id: { type: 'string', description: 'The ID of the project.' },
|
|
151
264
|
solution_id: { type: 'string', description: 'The ID of the solution to generate a prototype from.' },
|
|
265
|
+
coding_agent: { type: 'string', enum: ['bifocal', 'client'], description: 'Who will build the prototype. "bifocal" (default) queues it to Bifocal\'s agent. "client" returns the spec for the calling agent to build locally.' },
|
|
152
266
|
},
|
|
153
267
|
required: ['project_id', 'solution_id'],
|
|
154
268
|
},
|
|
155
269
|
},
|
|
270
|
+
{
|
|
271
|
+
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 — 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.',
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
properties: {
|
|
276
|
+
prototype_id: { type: 'string', description: 'The ID of the prototype to update.' },
|
|
277
|
+
message: { type: 'string', description: 'The edit instruction.' },
|
|
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.' },
|
|
279
|
+
},
|
|
280
|
+
required: ['prototype_id', 'message'],
|
|
281
|
+
},
|
|
282
|
+
},
|
|
156
283
|
{
|
|
157
284
|
name: 'get_prototypes',
|
|
158
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.',
|
|
@@ -195,10 +322,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
195
322
|
const { name, arguments: args } = request.params;
|
|
196
323
|
try {
|
|
197
324
|
if (name === 'import_prototype') {
|
|
198
|
-
const { project_id, prototype_name, zip_path } = args;
|
|
325
|
+
const { project_id, prototype_name, zip_path, prototype_id: existingPrototypeId } = args;
|
|
199
326
|
const filename = zip_path.split('/').pop() || 'prototype.zip';
|
|
200
327
|
// Step 1: get presigned upload URL
|
|
201
|
-
const uploadResult = await importPrototypeUploadUrl(project_id, prototype_name,
|
|
328
|
+
const uploadResult = await importPrototypeUploadUrl(project_id, filename, prototype_name, existingPrototypeId);
|
|
202
329
|
if (uploadResult.warning) {
|
|
203
330
|
// Surface warning but continue
|
|
204
331
|
console.warn('[import_prototype]', uploadResult.warning);
|
|
@@ -353,14 +480,42 @@ Bifocal requires a Vite + React single-page app containing only the experience b
|
|
|
353
480
|
const quotes = await getQuotes(project_id, insight_id);
|
|
354
481
|
return { content: [{ type: 'text', text: JSON.stringify(quotes, null, 2) }] };
|
|
355
482
|
}
|
|
483
|
+
if (name === 'create_context') {
|
|
484
|
+
const { project_id, name: contextName, type, content, description } = args;
|
|
485
|
+
const result = await createContext(project_id, contextName, type, content, description);
|
|
486
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
487
|
+
}
|
|
488
|
+
if (name === 'get_contexts') {
|
|
489
|
+
const { project_id } = args;
|
|
490
|
+
const contexts = await getContexts(project_id);
|
|
491
|
+
return { content: [{ type: 'text', text: JSON.stringify(contexts, null, 2) }] };
|
|
492
|
+
}
|
|
356
493
|
if (name === 'generate_solution') {
|
|
357
|
-
const { project_id, insight_ids,
|
|
358
|
-
const
|
|
494
|
+
const { project_id, insight_ids, goal, constraints, context_ids } = args;
|
|
495
|
+
const prototypes = await getPrototypes(project_id);
|
|
496
|
+
const basePrototype = prototypes.find(p => p.parent_prototype_id === null && p.status === 'ready');
|
|
497
|
+
if (!basePrototype) {
|
|
498
|
+
throw new Error('No base prototype found for this project. A ready prototype with no parent is required.');
|
|
499
|
+
}
|
|
500
|
+
const result = await generateSolution(project_id, insight_ids, basePrototype.id, goal, constraints, context_ids);
|
|
359
501
|
return { content: [{ type: 'text', text: JSON.stringify({
|
|
360
502
|
...result,
|
|
361
503
|
message: 'Solution is being generated. Use get_solutions to check when it appears — typically takes 1-2 minutes.',
|
|
362
504
|
}, null, 2) }] };
|
|
363
505
|
}
|
|
506
|
+
if (name === 'create_solution') {
|
|
507
|
+
const { project_id, title, brief, category, barriers_addressed, interventions, assumptions, open_concerns, insight_ids } = args;
|
|
508
|
+
const result = await createSolution(project_id, title, brief, category, barriers_addressed, interventions, assumptions, open_concerns, insight_ids);
|
|
509
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
510
|
+
...result,
|
|
511
|
+
message: 'Solution created. Call generate_prototype to build a prototype from it.',
|
|
512
|
+
}, null, 2) }] };
|
|
513
|
+
}
|
|
514
|
+
if (name === 'update_solution') {
|
|
515
|
+
const { project_id, solution_id, title, brief, category, barriers_addressed, interventions, assumptions, open_concerns, insight_ids } = args;
|
|
516
|
+
const result = await updateSolution(project_id, solution_id, { title, brief, category, barriers_addressed, interventions, assumptions, open_concerns, insight_ids });
|
|
517
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...result, message: 'Solution updated.' }, null, 2) }] };
|
|
518
|
+
}
|
|
364
519
|
if (name === 'get_solutions') {
|
|
365
520
|
const { project_id } = args;
|
|
366
521
|
const solutions = await getSolutions(project_id);
|
|
@@ -372,10 +527,40 @@ Bifocal requires a Vite + React single-page app containing only the experience b
|
|
|
372
527
|
return { content: [{ type: 'text', text: JSON.stringify(solution, null, 2) }] };
|
|
373
528
|
}
|
|
374
529
|
if (name === 'generate_prototype') {
|
|
375
|
-
const { project_id, solution_id } = args;
|
|
376
|
-
const result = await generatePrototype(project_id, solution_id);
|
|
530
|
+
const { project_id, solution_id, coding_agent } = args;
|
|
531
|
+
const result = await generatePrototype(project_id, solution_id, coding_agent);
|
|
532
|
+
if (coding_agent === 'client') {
|
|
533
|
+
const clientResult = result;
|
|
534
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
535
|
+
...clientResult,
|
|
536
|
+
instructions: [
|
|
537
|
+
`1. Call export_prototype with project_id and base_prototype_id to download the base code.`,
|
|
538
|
+
`2. Unzip and implement the changes described in solution.interventions. Apply org contexts (design system, brand guidelines) if provided.`,
|
|
539
|
+
`3. Zip the result and call import_prototype with project_id, zip_path, and prototype_id (${clientResult.prototype_id}).`,
|
|
540
|
+
],
|
|
541
|
+
}, null, 2) }] };
|
|
542
|
+
}
|
|
377
543
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
378
544
|
}
|
|
545
|
+
if (name === 'update_prototype') {
|
|
546
|
+
const { prototype_id, message, coding_agent } = args;
|
|
547
|
+
if (coding_agent === 'client') {
|
|
548
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
549
|
+
prototype_id,
|
|
550
|
+
edit_instruction: message,
|
|
551
|
+
instructions: [
|
|
552
|
+
`1. Call export_prototype to download the current prototype code.`,
|
|
553
|
+
`2. Unzip and apply the edit instruction above.`,
|
|
554
|
+
`3. Zip the result and call import_prototype with project_id, zip_path, and prototype_id (${prototype_id}).`,
|
|
555
|
+
],
|
|
556
|
+
}, null, 2) }] };
|
|
557
|
+
}
|
|
558
|
+
const result = await updatePrototype(prototype_id, message);
|
|
559
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
560
|
+
...result,
|
|
561
|
+
message: 'Edit queued. Call get_prototype to poll until status returns to "ready".',
|
|
562
|
+
}, null, 2) }] };
|
|
563
|
+
}
|
|
379
564
|
if (name === 'get_prototypes') {
|
|
380
565
|
const { project_id } = args;
|
|
381
566
|
const prototypes = await getPrototypes(project_id);
|
|
@@ -407,6 +592,117 @@ Bifocal requires a Vite + React single-page app containing only the experience b
|
|
|
407
592
|
};
|
|
408
593
|
}
|
|
409
594
|
});
|
|
595
|
+
const PRIMER = `# Bifocal — What It Is and How to Use It
|
|
596
|
+
|
|
597
|
+
## What is Bifocal?
|
|
598
|
+
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.
|
|
599
|
+
|
|
600
|
+
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.
|
|
601
|
+
|
|
602
|
+
## Core Concepts
|
|
603
|
+
|
|
604
|
+
**Project**
|
|
605
|
+
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.
|
|
606
|
+
|
|
607
|
+
**Feedback**
|
|
608
|
+
Raw user research uploaded to a project — interview transcripts, session notes, usability findings. Bifocal processes this into structured insights.
|
|
609
|
+
|
|
610
|
+
**Insight**
|
|
611
|
+
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.
|
|
612
|
+
|
|
613
|
+
**Solution**
|
|
614
|
+
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.
|
|
615
|
+
|
|
616
|
+
**Prototype**
|
|
617
|
+
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.
|
|
618
|
+
|
|
619
|
+
**Context**
|
|
620
|
+
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.
|
|
621
|
+
|
|
622
|
+
## Data Model
|
|
623
|
+
\`\`\`
|
|
624
|
+
Project
|
|
625
|
+
└── Feedback (raw research)
|
|
626
|
+
└── Insights (structured findings, ordered by mention_count)
|
|
627
|
+
└── Contexts (design system, brand guidelines)
|
|
628
|
+
└── Solutions (hypotheses built from insights)
|
|
629
|
+
└── Prototype (deployed web app)
|
|
630
|
+
\`\`\`
|
|
631
|
+
|
|
632
|
+
## Standard Workflow
|
|
633
|
+
|
|
634
|
+
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.
|
|
635
|
+
|
|
636
|
+
2. **Plan** — select 1–3 insights to address. Consider the user's goal and any constraints. Optionally call \`get_contexts\` to understand design constraints.
|
|
637
|
+
|
|
638
|
+
3. **Generate a solution** — call \`generate_solution\` with the selected insight IDs, a goal, and any constraints. This is async — the solution appears in \`get_solutions\` within 1–2 minutes. Always call \`get_solution\` after to read the full intervention detail.
|
|
639
|
+
|
|
640
|
+
4. **Build a prototype** — call \`generate_prototype\` with the solution ID. Bifocal's coding agent builds a working app. This is async — poll \`get_prototype\` until status is \`ready\` (typically 3–5 minutes). The \`published_url\` is the live prototype.
|
|
641
|
+
|
|
642
|
+
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.
|
|
643
|
+
|
|
644
|
+
## Tool Reference by Category
|
|
645
|
+
|
|
646
|
+
**Finding your project**
|
|
647
|
+
- \`list_projects\` — start here, always
|
|
648
|
+
|
|
649
|
+
**Understanding the research**
|
|
650
|
+
- \`get_insights\` — structured findings ordered by mention count; read these before generating anything
|
|
651
|
+
- \`get_quotes\` — raw supporting quotes for a specific insight; use when you need more evidence
|
|
652
|
+
|
|
653
|
+
**Design system / constraints**
|
|
654
|
+
- \`get_contexts\` — read design tokens, component specs, brand guidelines for the project
|
|
655
|
+
- \`create_context\` — add new context (design system, brand guidelines, etc.)
|
|
656
|
+
|
|
657
|
+
**Solutions**
|
|
658
|
+
- \`generate_solution\` — async; requires insight IDs, optional goal + constraints; always elicit these from the user first
|
|
659
|
+
- \`get_solutions\` — list all solutions; use to check if generation is complete
|
|
660
|
+
- \`get_solution\` — full detail including interventions and page changes; always call after generation
|
|
661
|
+
- \`create_solution\` — manually specify a solution without using the generator
|
|
662
|
+
- \`update_solution\` — modify an existing solution
|
|
663
|
+
|
|
664
|
+
**Prototypes**
|
|
665
|
+
- \`generate_prototype\` — async; builds from a solution; use \`coding_agent: "bifocal"\` (default) or \`"client"\` (to build locally)
|
|
666
|
+
- \`get_prototype\` — full detail including sitemap; use to poll for ready status
|
|
667
|
+
- \`get_prototypes\` — list all prototypes for a project
|
|
668
|
+
- \`update_prototype\` — async edit instruction to an existing prototype; use for targeted refinements
|
|
669
|
+
- \`export_prototype\` — download source as ZIP for local editing
|
|
670
|
+
- \`import_prototype\` — upload a locally built or edited ZIP; always call \`get_import_instructions\` first
|
|
671
|
+
|
|
672
|
+
**Feedback**
|
|
673
|
+
- \`add_feedback\` — add text or PDF research to a project, linked to a prototype
|
|
674
|
+
|
|
675
|
+
## Key Behaviors to Know
|
|
676
|
+
|
|
677
|
+
- **Async operations**: \`generate_solution\`, \`generate_prototype\`, and \`update_prototype\` are all async. After calling them, poll the corresponding getter (\`get_solutions\`/\`get_prototype\`) until the result appears. Do not assume they complete immediately.
|
|
678
|
+
|
|
679
|
+
- **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.
|
|
680
|
+
|
|
681
|
+
- **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.
|
|
682
|
+
|
|
683
|
+
- **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.
|
|
684
|
+
|
|
685
|
+
- **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.
|
|
686
|
+
`;
|
|
687
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
688
|
+
resources: [
|
|
689
|
+
{
|
|
690
|
+
uri: 'bifocal://primer',
|
|
691
|
+
name: 'Bifocal Primer',
|
|
692
|
+
description: 'Start here — overview of what Bifocal is, core concepts, data model, standard workflow, and tool reference. Read this before using any tools.',
|
|
693
|
+
mimeType: 'text/markdown',
|
|
694
|
+
},
|
|
695
|
+
],
|
|
696
|
+
}));
|
|
697
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
698
|
+
const { uri } = request.params;
|
|
699
|
+
if (uri === 'bifocal://primer') {
|
|
700
|
+
return {
|
|
701
|
+
contents: [{ uri, mimeType: 'text/markdown', text: PRIMER }],
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
705
|
+
});
|
|
410
706
|
async function main() {
|
|
411
707
|
const transport = new StdioServerTransport();
|
|
412
708
|
await server.connect(transport);
|