@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.
@@ -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: 'PATCH',
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: { 'Content-Type': 'application/zip' },
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: `Bifocal requires a Vite + React single-page app. Follow these steps to prepare your zip:
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: "Is this project built with Next.js or Vite?"
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
- - Run: "Convert this project from Next.js to a Vite + React single-page app with react-router-dom for navigation. Remove all server-side rendering, API routes, and any use of getServerSideProps or getStaticProps. Keep all the UI and logic but make everything client-side only. Each major section of the app should be its own route (e.g. /home, /dashboard, /settings). The build should produce a dist/ folder when you run npm run build."
196
- - Then verify: "Run npm run build and let me know if it succeeds or if there are any errors." Fix any errors before continuing.
197
- - Then: "Create a ZIP of the project excluding node_modules, .git, dist, and any .env files."
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: "Add react-router-dom to this project. Break the main sections of the UI into separate routes using <BrowserRouter> in main.tsx and <Routes>/<Route> in App.tsx. Each major section should have its own URL path."
202
- - Verify: "Run npm run build and let me know if it succeeds or if there are any errors." Fix any errors before continuing.
203
- - Then: "Create a ZIP of the project excluding node_modules, .git, dist, and any .env files."`,
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: `Bifocal requires a Vite + React project. Next.js is not supported.
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
- Your project should have package.json, index.html, and src/ at the root, and npm run build should output to a dist/ or build/ folder.
294
+ 4. Stub out external services auth, feature flags, analytics, payments, real-time connections with simple no-op replacements.
215
295
 
216
- Create a ZIP of the project excluding node_modules, .git, dist, and any .env files.`,
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.2",
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
  }