@bifocal/mcp 0.1.1 → 0.1.3
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 +51 -2
- package/dist/index.js +177 -2
- 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,3 +83,39 @@ 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 importPrototypeUploadUrl(projectId, prototypeName, filename) {
|
|
87
|
+
const response = await fetch(`${API_URL}/api/projects/${projectId}/prototypes/import/upload-url`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: headers(),
|
|
90
|
+
body: JSON.stringify({ prototype_name: prototypeName, filename }),
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
const error = await response.json().catch(() => ({}));
|
|
94
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
95
|
+
}
|
|
96
|
+
return response.json();
|
|
97
|
+
}
|
|
98
|
+
export async function addFeedbackText(projectId, prototypeId, transcript, sessionName) {
|
|
99
|
+
const response = await fetch(`${API_URL}/api/projects/${projectId}/feedback`, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: headers(),
|
|
102
|
+
body: JSON.stringify({ prototype_id: prototypeId, transcript, session_name: sessionName }),
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
const error = await response.json().catch(() => ({}));
|
|
106
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
107
|
+
}
|
|
108
|
+
return response.json();
|
|
109
|
+
}
|
|
110
|
+
export async function importPrototypeConfirm(projectId, prototypeId, contextId, filename, s3Key, s3Url) {
|
|
111
|
+
const response = await fetch(`${API_URL}/api/projects/${projectId}/prototypes/import/confirm`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: headers(),
|
|
114
|
+
body: JSON.stringify({ prototype_id: prototypeId, context_id: contextId, filename, s3_key: s3Key, s3_url: s3Url }),
|
|
115
|
+
});
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
const error = await response.json().catch(() => ({}));
|
|
118
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
119
|
+
}
|
|
120
|
+
return response.json();
|
|
121
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,54 @@
|
|
|
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, 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 } 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: {} } });
|
|
9
9
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
10
10
|
tools: [
|
|
11
|
+
{
|
|
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.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
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.' },
|
|
19
|
+
zip_path: { type: 'string', description: 'Absolute path to the zip file on the local filesystem.' },
|
|
20
|
+
},
|
|
21
|
+
required: ['project_id', 'prototype_name', 'zip_path'],
|
|
22
|
+
},
|
|
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
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'get_import_instructions',
|
|
40
|
+
description: 'Get platform-specific instructions for preparing a prototype zip file before importing it into Bifocal. Always call this before import_prototype.',
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
platform: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
enum: ['claudecode', 'lovable', 'figmamake', 'other'],
|
|
47
|
+
description: 'The platform the prototype was built with.',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
required: ['platform'],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
11
53
|
{
|
|
12
54
|
name: 'create_project',
|
|
13
55
|
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.',
|
|
@@ -61,6 +103,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
61
103
|
required: ['project_id', 'insight_id'],
|
|
62
104
|
},
|
|
63
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
|
+
},
|
|
64
121
|
{
|
|
65
122
|
name: 'get_solutions',
|
|
66
123
|
description: 'Get all solutions for a Bifocal project. Returns title, brief, barriers addressed, and prototype link (if one was generated) for each solution.',
|
|
@@ -125,6 +182,112 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
125
182
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
126
183
|
const { name, arguments: args } = request.params;
|
|
127
184
|
try {
|
|
185
|
+
if (name === 'import_prototype') {
|
|
186
|
+
const { project_id, prototype_name, zip_path } = args;
|
|
187
|
+
const filename = zip_path.split('/').pop() || 'prototype.zip';
|
|
188
|
+
// Step 1: get presigned upload URL
|
|
189
|
+
const uploadResult = await importPrototypeUploadUrl(project_id, prototype_name, filename);
|
|
190
|
+
if (uploadResult.warning) {
|
|
191
|
+
// Surface warning but continue
|
|
192
|
+
console.warn('[import_prototype]', uploadResult.warning);
|
|
193
|
+
}
|
|
194
|
+
// Step 2: read zip and upload to S3
|
|
195
|
+
const { readFile } = await import('fs/promises');
|
|
196
|
+
const zipBuffer = await readFile(zip_path);
|
|
197
|
+
const uploadResponse = await fetch(uploadResult.upload_url, {
|
|
198
|
+
method: 'PUT',
|
|
199
|
+
headers: {
|
|
200
|
+
'Content-Type': 'application/zip',
|
|
201
|
+
'Content-Encoding': 'identity',
|
|
202
|
+
},
|
|
203
|
+
body: zipBuffer,
|
|
204
|
+
});
|
|
205
|
+
if (!uploadResponse.ok) {
|
|
206
|
+
throw new Error(`Failed to upload zip to S3: ${uploadResponse.status}`);
|
|
207
|
+
}
|
|
208
|
+
// Step 3: confirm import and queue processing
|
|
209
|
+
const confirmResult = await importPrototypeConfirm(project_id, uploadResult.prototype_id, uploadResult.context_id, filename, uploadResult.s3_key, uploadResult.s3_url);
|
|
210
|
+
const response = {
|
|
211
|
+
prototype_id: confirmResult.prototype_id,
|
|
212
|
+
status: confirmResult.status,
|
|
213
|
+
message: 'Prototype uploaded and queued for processing. It will be ready in a few minutes.',
|
|
214
|
+
};
|
|
215
|
+
if (uploadResult.warning)
|
|
216
|
+
response.warning = uploadResult.warning;
|
|
217
|
+
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
|
218
|
+
}
|
|
219
|
+
if (name === 'add_feedback') {
|
|
220
|
+
const { project_id, prototype_id, source, session_name } = args;
|
|
221
|
+
const ext = source.split('.').pop()?.toLowerCase() || '';
|
|
222
|
+
const isFilePath = source.startsWith('/') || source.startsWith('./') || source.startsWith('~');
|
|
223
|
+
if (isFilePath && ext === 'pdf') {
|
|
224
|
+
// PDF: extract text and submit as transcript
|
|
225
|
+
const { readFile } = await import('fs/promises');
|
|
226
|
+
const pdfBuffer = await readFile(source);
|
|
227
|
+
const pdfParseModule = await import('pdf-parse');
|
|
228
|
+
const pdfParse = pdfParseModule.default ?? pdfParseModule;
|
|
229
|
+
const parsed = await pdfParse(pdfBuffer);
|
|
230
|
+
const result = await addFeedbackText(project_id, prototype_id, parsed.text, session_name);
|
|
231
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...result, note: 'Text extracted from PDF.' }, null, 2) }] };
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
// Plain text
|
|
235
|
+
const result = await addFeedbackText(project_id, prototype_id, source, session_name);
|
|
236
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (name === 'get_import_instructions') {
|
|
240
|
+
const { platform } = args;
|
|
241
|
+
const instructions = {
|
|
242
|
+
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.
|
|
243
|
+
|
|
244
|
+
Bifocal requires a Vite + React single-page app containing only the experience being tested. Follow these steps:
|
|
245
|
+
|
|
246
|
+
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.
|
|
247
|
+
|
|
248
|
+
2. Ask: "Is this project built with Next.js or Vite?"
|
|
249
|
+
|
|
250
|
+
If Next.js:
|
|
251
|
+
- Convert to a Vite + React single-page app with react-router-dom. Remove all SSR, API routes, getServerSideProps, and getStaticProps.
|
|
252
|
+
- Verify: run npm run build and fix any errors before continuing.
|
|
253
|
+
- Create a ZIP excluding node_modules, .git, dist, and any .env files or credentials.
|
|
254
|
+
|
|
255
|
+
If Vite:
|
|
256
|
+
- Ask: "Does this project use react-router-dom so that each major section has its own URL path?"
|
|
257
|
+
- If no: add react-router-dom with each major section as its own route.
|
|
258
|
+
- Verify: run npm run build and fix any errors before continuing.
|
|
259
|
+
- Create a ZIP excluding node_modules, .git, dist, and any .env files or credentials.
|
|
260
|
+
|
|
261
|
+
3. Replace any API calls or environment variables with hardcoded fixture data. The prototype must work with no backend and no network requests.
|
|
262
|
+
|
|
263
|
+
4. Stub out external services — auth, feature flags, analytics, payments, real-time connections — with simple no-op replacements.`,
|
|
264
|
+
lovable: `To export from Lovable: open your project → top-right menu → Export → Download ZIP.
|
|
265
|
+
|
|
266
|
+
Do not manually zip the project files — the Export button handles cleanup automatically.`,
|
|
267
|
+
figmamake: `To export from Figma Make:
|
|
268
|
+
1. In Figma Make, go to Prototype Settings → GitHub and push your project to a GitHub repo.
|
|
269
|
+
2. Go to your GitHub repo → click the green Code button → Download ZIP.
|
|
270
|
+
|
|
271
|
+
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.`,
|
|
272
|
+
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.
|
|
273
|
+
|
|
274
|
+
Bifocal requires a Vite + React single-page app containing only the experience being tested.
|
|
275
|
+
|
|
276
|
+
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.
|
|
277
|
+
|
|
278
|
+
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.
|
|
279
|
+
|
|
280
|
+
3. Replace any API calls or environment variables with hardcoded fixture data. The prototype must work with no backend and no network requests.
|
|
281
|
+
|
|
282
|
+
4. Stub out external services — auth, feature flags, analytics, payments, real-time connections — with simple no-op replacements.
|
|
283
|
+
|
|
284
|
+
5. Verify: run npm run build and fix any errors before continuing.
|
|
285
|
+
|
|
286
|
+
6. Create a ZIP excluding node_modules, .git, dist, and any .env files or credentials.`,
|
|
287
|
+
};
|
|
288
|
+
const text = instructions[platform] ?? instructions.other;
|
|
289
|
+
return { content: [{ type: 'text', text }] };
|
|
290
|
+
}
|
|
128
291
|
if (name === 'update_project') {
|
|
129
292
|
const { project_id, name: projectName, description } = args;
|
|
130
293
|
const project = await updateProject(project_id, { name: projectName, description });
|
|
@@ -158,7 +321,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
158
321
|
}
|
|
159
322
|
}
|
|
160
323
|
const project = await createProject(projectName, description);
|
|
161
|
-
|
|
324
|
+
const response = {
|
|
325
|
+
...project,
|
|
326
|
+
next_step: "Ask the user if they have a prototype they'd like to import — projects require one to be useful. If yes, call get_import_instructions first to get platform-specific requirements for preparing the zip, then call import_prototype.",
|
|
327
|
+
};
|
|
328
|
+
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
|
162
329
|
}
|
|
163
330
|
if (name === 'list_projects') {
|
|
164
331
|
const projects = await listProjects();
|
|
@@ -174,6 +341,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
174
341
|
const quotes = await getQuotes(project_id, insight_id);
|
|
175
342
|
return { content: [{ type: 'text', text: JSON.stringify(quotes, null, 2) }] };
|
|
176
343
|
}
|
|
344
|
+
if (name === 'generate_solution') {
|
|
345
|
+
const { project_id, insight_ids, prototype_id, goal, constraints } = args;
|
|
346
|
+
const result = await generateSolution(project_id, insight_ids, prototype_id, goal, constraints);
|
|
347
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
348
|
+
...result,
|
|
349
|
+
message: 'Solution is being generated. Use get_solutions to check when it appears — typically takes 1-2 minutes.',
|
|
350
|
+
}, null, 2) }] };
|
|
351
|
+
}
|
|
177
352
|
if (name === 'get_solutions') {
|
|
178
353
|
const { project_id } = args;
|
|
179
354
|
const solutions = await getSolutions(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.3",
|
|
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
|
}
|