@bifocal/mcp 0.1.1 → 0.1.2

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.
@@ -70,3 +70,27 @@ export async function getPrototype(projectId, prototypeId) {
70
70
  export async function exportPrototype(projectId, prototypeId) {
71
71
  return get(`/api/projects/${projectId}/prototypes/${prototypeId}/download-zip`);
72
72
  }
73
+ export async function importPrototypeUploadUrl(projectId, prototypeName, filename) {
74
+ const response = await fetch(`${API_URL}/api/projects/${projectId}/prototypes/import/upload-url`, {
75
+ method: 'POST',
76
+ headers: headers(),
77
+ body: JSON.stringify({ prototype_name: prototypeName, filename }),
78
+ });
79
+ if (!response.ok) {
80
+ const error = await response.json().catch(() => ({}));
81
+ throw new Error(error.error || `Request failed: ${response.status}`);
82
+ }
83
+ return response.json();
84
+ }
85
+ export async function importPrototypeConfirm(projectId, prototypeId, contextId, filename, s3Key, s3Url) {
86
+ const response = await fetch(`${API_URL}/api/projects/${projectId}/prototypes/import/confirm`, {
87
+ method: 'POST',
88
+ headers: headers(),
89
+ body: JSON.stringify({ prototype_id: prototypeId, context_id: contextId, filename, s3_key: s3Key, s3_url: s3Url }),
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
+ }
package/dist/index.js CHANGED
@@ -2,12 +2,40 @@
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, 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: 'get_import_instructions',
26
+ description: 'Get platform-specific instructions for preparing a prototype zip file before importing it into Bifocal. Always call this before import_prototype.',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {
30
+ platform: {
31
+ type: 'string',
32
+ enum: ['claudecode', 'lovable', 'figmamake', 'other'],
33
+ description: 'The platform the prototype was built with.',
34
+ },
35
+ },
36
+ required: ['platform'],
37
+ },
38
+ },
11
39
  {
12
40
  name: 'create_project',
13
41
  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.',
@@ -125,6 +153,71 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
125
153
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
126
154
  const { name, arguments: args } = request.params;
127
155
  try {
156
+ if (name === 'import_prototype') {
157
+ const { project_id, prototype_name, zip_path } = args;
158
+ const filename = zip_path.split('/').pop() || 'prototype.zip';
159
+ // Step 1: get presigned upload URL
160
+ const uploadResult = await importPrototypeUploadUrl(project_id, prototype_name, filename);
161
+ if (uploadResult.warning) {
162
+ // Surface warning but continue
163
+ console.warn('[import_prototype]', uploadResult.warning);
164
+ }
165
+ // Step 2: read zip and upload to S3
166
+ const { readFile } = await import('fs/promises');
167
+ const zipBuffer = await readFile(zip_path);
168
+ const uploadResponse = await fetch(uploadResult.upload_url, {
169
+ method: 'PUT',
170
+ headers: { 'Content-Type': 'application/zip' },
171
+ body: zipBuffer,
172
+ });
173
+ if (!uploadResponse.ok) {
174
+ throw new Error(`Failed to upload zip to S3: ${uploadResponse.status}`);
175
+ }
176
+ // Step 3: confirm import and queue processing
177
+ const confirmResult = await importPrototypeConfirm(project_id, uploadResult.prototype_id, uploadResult.context_id, filename, uploadResult.s3_key, uploadResult.s3_url);
178
+ const response = {
179
+ prototype_id: confirmResult.prototype_id,
180
+ status: confirmResult.status,
181
+ message: 'Prototype uploaded and queued for processing. It will be ready in a few minutes.',
182
+ };
183
+ if (uploadResult.warning)
184
+ response.warning = uploadResult.warning;
185
+ return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
186
+ }
187
+ if (name === 'get_import_instructions') {
188
+ const { platform } = args;
189
+ const instructions = {
190
+ claudecode: `Bifocal requires a Vite + React single-page app. Follow these steps to prepare your zip:
191
+
192
+ 1. Ask: "Is this project built with Next.js or Vite?"
193
+
194
+ 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."
198
+
199
+ If Vite:
200
+ - 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."`,
204
+ lovable: `To export from Lovable: open your project → top-right menu → Export → Download ZIP.
205
+
206
+ Do not manually zip the project files — the Export button handles cleanup automatically.`,
207
+ figmamake: `To export from Figma Make:
208
+ 1. In Figma Make, go to Prototype Settings → GitHub and push your project to a GitHub repo.
209
+ 2. Go to your GitHub repo → click the green Code button → Download ZIP.
210
+
211
+ 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.
213
+
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.
215
+
216
+ Create a ZIP of the project excluding node_modules, .git, dist, and any .env files.`,
217
+ };
218
+ const text = instructions[platform] ?? instructions.other;
219
+ return { content: [{ type: 'text', text }] };
220
+ }
128
221
  if (name === 'update_project') {
129
222
  const { project_id, name: projectName, description } = args;
130
223
  const project = await updateProject(project_id, { name: projectName, description });
@@ -158,7 +251,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
158
251
  }
159
252
  }
160
253
  const project = await createProject(projectName, description);
161
- return { content: [{ type: 'text', text: JSON.stringify(project, null, 2) }] };
254
+ const response = {
255
+ ...project,
256
+ 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.",
257
+ };
258
+ return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
162
259
  }
163
260
  if (name === 'list_projects') {
164
261
  const projects = await listProjects();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bifocal/mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Bifocal MCP server — access projects, insights, and prototypes from Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",