@bifocal/mcp 0.1.2 → 0.1.4
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 +38 -2
- package/dist/index.js +108 -13
- package/package.json +6 -3
package/dist/bifocalClient.js
CHANGED
|
@@ -17,9 +17,22 @@ async function get(path) {
|
|
|
17
17
|
}
|
|
18
18
|
return response.json();
|
|
19
19
|
}
|
|
20
|
+
export async function generateSolution(projectId, insightIds, prototypeId, goal, constraints) {
|
|
21
|
+
const response = await fetch(`${API_URL}/api/projects/${projectId}/solutions/generate`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: headers(),
|
|
24
|
+
body: JSON.stringify({ insightIds, prototypeId, goal, constraints }),
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const error = await response.json().catch(() => ({}));
|
|
28
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
29
|
+
}
|
|
30
|
+
const data = await response.json();
|
|
31
|
+
return { solution_id: data.solution_id, status: 'generating' };
|
|
32
|
+
}
|
|
20
33
|
export async function updateProject(projectId, updates) {
|
|
21
|
-
const response = await fetch(`${API_URL}/api/projects/${projectId}`, {
|
|
22
|
-
method: '
|
|
34
|
+
const response = await fetch(`${API_URL}/api/projects/${projectId}/update`, {
|
|
35
|
+
method: 'POST',
|
|
23
36
|
headers: headers(),
|
|
24
37
|
body: JSON.stringify(updates),
|
|
25
38
|
});
|
|
@@ -70,6 +83,17 @@ export async function getPrototype(projectId, prototypeId) {
|
|
|
70
83
|
export async function exportPrototype(projectId, prototypeId) {
|
|
71
84
|
return get(`/api/projects/${projectId}/prototypes/${prototypeId}/download-zip`);
|
|
72
85
|
}
|
|
86
|
+
export async function generatePrototype(projectId, solutionId) {
|
|
87
|
+
const response = await fetch(`${API_URL}/api/projects/${projectId}/solutions/${solutionId}/generate-prototype`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: headers(),
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const error = await response.json().catch(() => ({}));
|
|
93
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
94
|
+
}
|
|
95
|
+
return response.json();
|
|
96
|
+
}
|
|
73
97
|
export async function importPrototypeUploadUrl(projectId, prototypeName, filename) {
|
|
74
98
|
const response = await fetch(`${API_URL}/api/projects/${projectId}/prototypes/import/upload-url`, {
|
|
75
99
|
method: 'POST',
|
|
@@ -82,6 +106,18 @@ export async function importPrototypeUploadUrl(projectId, prototypeName, filenam
|
|
|
82
106
|
}
|
|
83
107
|
return response.json();
|
|
84
108
|
}
|
|
109
|
+
export async function addFeedbackText(projectId, prototypeId, transcript, sessionName) {
|
|
110
|
+
const response = await fetch(`${API_URL}/api/projects/${projectId}/feedback`, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: headers(),
|
|
113
|
+
body: JSON.stringify({ prototype_id: prototypeId, transcript, session_name: sessionName }),
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
const error = await response.json().catch(() => ({}));
|
|
117
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
118
|
+
}
|
|
119
|
+
return response.json();
|
|
120
|
+
}
|
|
85
121
|
export async function importPrototypeConfirm(projectId, prototypeId, contextId, filename, s3Key, s3Url) {
|
|
86
122
|
const response = await fetch(`${API_URL}/api/projects/${projectId}/prototypes/import/confirm`, {
|
|
87
123
|
method: 'POST',
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
-
import { createProject, updateProject, listProjects, importPrototypeUploadUrl, importPrototypeConfirm, getInsights, getQuotes, getSolutions, getSolution, getPrototypes, getPrototype, exportPrototype } from './bifocalClient.js';
|
|
5
|
+
import { createProject, updateProject, listProjects, importPrototypeUploadUrl, importPrototypeConfirm, addFeedbackText, generateSolution, getInsights, getQuotes, getSolutions, getSolution, getPrototypes, getPrototype, exportPrototype, generatePrototype } from './bifocalClient.js';
|
|
6
6
|
import { writeFile } from 'fs/promises';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
const server = new Server({ name: 'bifocal', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
@@ -21,6 +21,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
21
21
|
required: ['project_id', 'prototype_name', 'zip_path'],
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
|
+
{
|
|
25
|
+
name: 'add_feedback',
|
|
26
|
+
description: 'Add feedback to a Bifocal project. Accepts plain text or a path to a PDF file. Text is submitted directly; PDFs are parsed and text extracted before submitting.',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
project_id: { type: 'string', description: 'The ID of the project.' },
|
|
31
|
+
prototype_id: { type: 'string', description: 'The ID of the prototype the feedback is about.' },
|
|
32
|
+
source: { type: 'string', description: 'The feedback content — either plain text or a local path to a PDF file.' },
|
|
33
|
+
session_name: { type: 'string', description: 'Optional name for the feedback session.' },
|
|
34
|
+
},
|
|
35
|
+
required: ['project_id', 'prototype_id', 'source'],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
24
38
|
{
|
|
25
39
|
name: 'get_import_instructions',
|
|
26
40
|
description: 'Get platform-specific instructions for preparing a prototype zip file before importing it into Bifocal. Always call this before import_prototype.',
|
|
@@ -89,6 +103,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
89
103
|
required: ['project_id', 'insight_id'],
|
|
90
104
|
},
|
|
91
105
|
},
|
|
106
|
+
{
|
|
107
|
+
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.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
project_id: { type: 'string', description: 'The ID of the project.' },
|
|
113
|
+
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
|
+
goal: { type: 'string', description: 'Optional goal or focus for the solution.' },
|
|
116
|
+
constraints: { type: 'string', description: 'Optional constraints to consider.' },
|
|
117
|
+
},
|
|
118
|
+
required: ['project_id', 'insight_ids', 'prototype_id'],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
92
121
|
{
|
|
93
122
|
name: 'get_solutions',
|
|
94
123
|
description: 'Get all solutions for a Bifocal project. Returns title, brief, barriers addressed, and prototype link (if one was generated) for each solution.',
|
|
@@ -112,6 +141,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
112
141
|
required: ['project_id', 'solution_id'],
|
|
113
142
|
},
|
|
114
143
|
},
|
|
144
|
+
{
|
|
145
|
+
name: 'generate_prototype',
|
|
146
|
+
description: 'Generate a prototype from a solution. This queues an async build — call get_prototype with the returned prototype_id to check when status changes to "ready". If a prototype already exists for the solution, returns it immediately.',
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
project_id: { type: 'string', description: 'The ID of the project.' },
|
|
151
|
+
solution_id: { type: 'string', description: 'The ID of the solution to generate a prototype from.' },
|
|
152
|
+
},
|
|
153
|
+
required: ['project_id', 'solution_id'],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
115
156
|
{
|
|
116
157
|
name: 'get_prototypes',
|
|
117
158
|
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.',
|
|
@@ -167,7 +208,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
167
208
|
const zipBuffer = await readFile(zip_path);
|
|
168
209
|
const uploadResponse = await fetch(uploadResult.upload_url, {
|
|
169
210
|
method: 'PUT',
|
|
170
|
-
headers: {
|
|
211
|
+
headers: {
|
|
212
|
+
'Content-Type': 'application/zip',
|
|
213
|
+
'Content-Encoding': 'identity',
|
|
214
|
+
},
|
|
171
215
|
body: zipBuffer,
|
|
172
216
|
});
|
|
173
217
|
if (!uploadResponse.ok) {
|
|
@@ -184,23 +228,51 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
184
228
|
response.warning = uploadResult.warning;
|
|
185
229
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
|
186
230
|
}
|
|
231
|
+
if (name === 'add_feedback') {
|
|
232
|
+
const { project_id, prototype_id, source, session_name } = args;
|
|
233
|
+
const ext = source.split('.').pop()?.toLowerCase() || '';
|
|
234
|
+
const isFilePath = source.startsWith('/') || source.startsWith('./') || source.startsWith('~');
|
|
235
|
+
if (isFilePath && ext === 'pdf') {
|
|
236
|
+
// PDF: extract text and submit as transcript
|
|
237
|
+
const { readFile } = await import('fs/promises');
|
|
238
|
+
const pdfBuffer = await readFile(source);
|
|
239
|
+
const pdfParseModule = await import('pdf-parse');
|
|
240
|
+
const pdfParse = pdfParseModule.default ?? pdfParseModule;
|
|
241
|
+
const parsed = await pdfParse(pdfBuffer);
|
|
242
|
+
const result = await addFeedbackText(project_id, prototype_id, parsed.text, session_name);
|
|
243
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...result, note: 'Text extracted from PDF.' }, null, 2) }] };
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// Plain text
|
|
247
|
+
const result = await addFeedbackText(project_id, prototype_id, source, session_name);
|
|
248
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
187
251
|
if (name === 'get_import_instructions') {
|
|
188
252
|
const { platform } = args;
|
|
189
253
|
const instructions = {
|
|
190
|
-
claudecode:
|
|
254
|
+
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.
|
|
255
|
+
|
|
256
|
+
Bifocal requires a Vite + React single-page app containing only the experience being tested. Follow these steps:
|
|
191
257
|
|
|
192
|
-
1. Ask: "
|
|
258
|
+
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.
|
|
259
|
+
|
|
260
|
+
2. Ask: "Is this project built with Next.js or Vite?"
|
|
193
261
|
|
|
194
262
|
If Next.js:
|
|
195
|
-
-
|
|
196
|
-
-
|
|
197
|
-
-
|
|
263
|
+
- Convert to a Vite + React single-page app with react-router-dom. Remove all SSR, API routes, getServerSideProps, and getStaticProps.
|
|
264
|
+
- Verify: run npm run build and fix any errors before continuing.
|
|
265
|
+
- Create a ZIP excluding node_modules, .git, dist, and any .env files or credentials.
|
|
198
266
|
|
|
199
267
|
If Vite:
|
|
200
268
|
- Ask: "Does this project use react-router-dom so that each major section has its own URL path?"
|
|
201
|
-
- If no:
|
|
202
|
-
- Verify:
|
|
203
|
-
-
|
|
269
|
+
- If no: add react-router-dom with each major section as its own route.
|
|
270
|
+
- Verify: run npm run build and fix any errors before continuing.
|
|
271
|
+
- Create a ZIP excluding node_modules, .git, dist, and any .env files or credentials.
|
|
272
|
+
|
|
273
|
+
3. Replace any API calls or environment variables with hardcoded fixture data. The prototype must work with no backend and no network requests.
|
|
274
|
+
|
|
275
|
+
4. Stub out external services — auth, feature flags, analytics, payments, real-time connections — with simple no-op replacements.`,
|
|
204
276
|
lovable: `To export from Lovable: open your project → top-right menu → Export → Download ZIP.
|
|
205
277
|
|
|
206
278
|
Do not manually zip the project files — the Export button handles cleanup automatically.`,
|
|
@@ -209,11 +281,21 @@ Do not manually zip the project files — the Export button handles cleanup auto
|
|
|
209
281
|
2. Go to your GitHub repo → click the green Code button → Download ZIP.
|
|
210
282
|
|
|
211
283
|
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.`,
|
|
212
|
-
other:
|
|
284
|
+
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.
|
|
285
|
+
|
|
286
|
+
Bifocal requires a Vite + React single-page app containing only the experience being tested.
|
|
287
|
+
|
|
288
|
+
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.
|
|
289
|
+
|
|
290
|
+
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.
|
|
291
|
+
|
|
292
|
+
3. Replace any API calls or environment variables with hardcoded fixture data. The prototype must work with no backend and no network requests.
|
|
213
293
|
|
|
214
|
-
|
|
294
|
+
4. Stub out external services — auth, feature flags, analytics, payments, real-time connections — with simple no-op replacements.
|
|
215
295
|
|
|
216
|
-
|
|
296
|
+
5. Verify: run npm run build and fix any errors before continuing.
|
|
297
|
+
|
|
298
|
+
6. Create a ZIP excluding node_modules, .git, dist, and any .env files or credentials.`,
|
|
217
299
|
};
|
|
218
300
|
const text = instructions[platform] ?? instructions.other;
|
|
219
301
|
return { content: [{ type: 'text', text }] };
|
|
@@ -271,6 +353,14 @@ Create a ZIP of the project excluding node_modules, .git, dist, and any .env fil
|
|
|
271
353
|
const quotes = await getQuotes(project_id, insight_id);
|
|
272
354
|
return { content: [{ type: 'text', text: JSON.stringify(quotes, null, 2) }] };
|
|
273
355
|
}
|
|
356
|
+
if (name === 'generate_solution') {
|
|
357
|
+
const { project_id, insight_ids, prototype_id, goal, constraints } = args;
|
|
358
|
+
const result = await generateSolution(project_id, insight_ids, prototype_id, goal, constraints);
|
|
359
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
360
|
+
...result,
|
|
361
|
+
message: 'Solution is being generated. Use get_solutions to check when it appears — typically takes 1-2 minutes.',
|
|
362
|
+
}, null, 2) }] };
|
|
363
|
+
}
|
|
274
364
|
if (name === 'get_solutions') {
|
|
275
365
|
const { project_id } = args;
|
|
276
366
|
const solutions = await getSolutions(project_id);
|
|
@@ -281,6 +371,11 @@ Create a ZIP of the project excluding node_modules, .git, dist, and any .env fil
|
|
|
281
371
|
const solution = await getSolution(project_id, solution_id);
|
|
282
372
|
return { content: [{ type: 'text', text: JSON.stringify(solution, null, 2) }] };
|
|
283
373
|
}
|
|
374
|
+
if (name === 'generate_prototype') {
|
|
375
|
+
const { project_id, solution_id } = args;
|
|
376
|
+
const result = await generatePrototype(project_id, solution_id);
|
|
377
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
378
|
+
}
|
|
284
379
|
if (name === 'get_prototypes') {
|
|
285
380
|
const { project_id } = args;
|
|
286
381
|
const prototypes = await getPrototypes(project_id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bifocal/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Bifocal MCP server — access projects, insights, and prototypes from Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
"prepublishOnly": "tsc"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
21
|
+
"@types/pdf-parse": "^1.1.5",
|
|
22
|
+
"pdf-parse": "^2.4.5"
|
|
21
23
|
},
|
|
22
24
|
"devDependencies": {
|
|
23
25
|
"@types/node": "^20.0.0",
|
|
24
26
|
"typescript": "^5.3.2"
|
|
25
|
-
}
|
|
27
|
+
},
|
|
28
|
+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
26
29
|
}
|