@bifocal/mcp 0.1.2 → 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.
@@ -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
  });
@@ -82,6 +95,18 @@ export async function importPrototypeUploadUrl(projectId, prototypeName, filenam
82
95
  }
83
96
  return response.json();
84
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
+ }
85
110
  export async function importPrototypeConfirm(projectId, prototypeId, contextId, filename, s3Key, s3Url) {
86
111
  const response = await fetch(`${API_URL}/api/projects/${projectId}/prototypes/import/confirm`, {
87
112
  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 } 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.',
@@ -167,7 +196,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
167
196
  const zipBuffer = await readFile(zip_path);
168
197
  const uploadResponse = await fetch(uploadResult.upload_url, {
169
198
  method: 'PUT',
170
- headers: { 'Content-Type': 'application/zip' },
199
+ headers: {
200
+ 'Content-Type': 'application/zip',
201
+ 'Content-Encoding': 'identity',
202
+ },
171
203
  body: zipBuffer,
172
204
  });
173
205
  if (!uploadResponse.ok) {
@@ -184,23 +216,51 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
184
216
  response.warning = uploadResult.warning;
185
217
  return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
186
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
+ }
187
239
  if (name === 'get_import_instructions') {
188
240
  const { platform } = args;
189
241
  const instructions = {
190
- claudecode: `Bifocal requires a Vite + React single-page app. Follow these steps to prepare your zip:
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.
191
243
 
192
- 1. Ask: "Is this project built with Next.js or Vite?"
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?"
193
249
 
194
250
  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."
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.
198
254
 
199
255
  If Vite:
200
256
  - 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."`,
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.`,
204
264
  lovable: `To export from Lovable: open your project → top-right menu → Export → Download ZIP.
205
265
 
206
266
  Do not manually zip the project files — the Export button handles cleanup automatically.`,
@@ -209,11 +269,21 @@ Do not manually zip the project files — the Export button handles cleanup auto
209
269
  2. Go to your GitHub repo → click the green Code button → Download ZIP.
210
270
 
211
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.`,
212
- other: `Bifocal requires a Vite + React project. Next.js is not supported.
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.
213
279
 
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.
280
+ 3. Replace any API calls or environment variables with hardcoded fixture data. The prototype must work with no backend and no network requests.
215
281
 
216
- Create a ZIP of the project excluding node_modules, .git, dist, and any .env files.`,
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.`,
217
287
  };
218
288
  const text = instructions[platform] ?? instructions.other;
219
289
  return { content: [{ type: 'text', text }] };
@@ -271,6 +341,14 @@ Create a ZIP of the project excluding node_modules, .git, dist, and any .env fil
271
341
  const quotes = await getQuotes(project_id, insight_id);
272
342
  return { content: [{ type: 'text', text: JSON.stringify(quotes, null, 2) }] };
273
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
+ }
274
352
  if (name === 'get_solutions') {
275
353
  const { project_id } = args;
276
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.2",
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
  }