@bifocal/mcp 0.1.0
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 +72 -0
- package/dist/index.js +225 -0
- package/package.json +26 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const API_KEY = process.env.BIFOCAL_API_KEY;
|
|
2
|
+
const API_URL = process.env.BIFOCAL_API_URL || 'https://api.bifocal.ai';
|
|
3
|
+
if (!API_KEY) {
|
|
4
|
+
throw new Error('BIFOCAL_API_KEY environment variable is required');
|
|
5
|
+
}
|
|
6
|
+
function headers() {
|
|
7
|
+
return {
|
|
8
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
async function get(path) {
|
|
13
|
+
const response = await fetch(`${API_URL}${path}`, { headers: headers() });
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
const error = await response.json().catch(() => ({}));
|
|
16
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
17
|
+
}
|
|
18
|
+
return response.json();
|
|
19
|
+
}
|
|
20
|
+
export async function updateProject(projectId, updates) {
|
|
21
|
+
const response = await fetch(`${API_URL}/api/projects/${projectId}`, {
|
|
22
|
+
method: 'PATCH',
|
|
23
|
+
headers: headers(),
|
|
24
|
+
body: JSON.stringify(updates),
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const error = await response.json().catch(() => ({}));
|
|
28
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
29
|
+
}
|
|
30
|
+
return response.json();
|
|
31
|
+
}
|
|
32
|
+
export async function createProject(name, description) {
|
|
33
|
+
const response = await fetch(`${API_URL}/api/projects`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: headers(),
|
|
36
|
+
body: JSON.stringify({ name, description }),
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const error = await response.json().catch(() => ({}));
|
|
40
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
41
|
+
}
|
|
42
|
+
return response.json();
|
|
43
|
+
}
|
|
44
|
+
export async function listProjects() {
|
|
45
|
+
const data = await get('/api/projects');
|
|
46
|
+
return data.projects;
|
|
47
|
+
}
|
|
48
|
+
export async function getInsights(projectId) {
|
|
49
|
+
const data = await get(`/api/projects/${projectId}/insights`);
|
|
50
|
+
return data.insights;
|
|
51
|
+
}
|
|
52
|
+
export async function getQuotes(projectId, insightId) {
|
|
53
|
+
const data = await get(`/api/projects/${projectId}/insights/${insightId}/quotes`);
|
|
54
|
+
return data.quotes;
|
|
55
|
+
}
|
|
56
|
+
export async function getSolutions(projectId) {
|
|
57
|
+
const data = await get(`/api/projects/${projectId}/solutions`);
|
|
58
|
+
return data.solutions;
|
|
59
|
+
}
|
|
60
|
+
export async function getSolution(projectId, solutionId) {
|
|
61
|
+
return get(`/api/projects/${projectId}/solutions/${solutionId}`);
|
|
62
|
+
}
|
|
63
|
+
export async function getPrototypes(projectId) {
|
|
64
|
+
const data = await get(`/api/projects/${projectId}/prototypes`);
|
|
65
|
+
return data.prototypes;
|
|
66
|
+
}
|
|
67
|
+
export async function getPrototype(projectId, prototypeId) {
|
|
68
|
+
return get(`/api/projects/${projectId}/prototypes/${prototypeId}`);
|
|
69
|
+
}
|
|
70
|
+
export async function exportPrototype(projectId, prototypeId) {
|
|
71
|
+
return get(`/api/projects/${projectId}/prototypes/${prototypeId}/download-zip`);
|
|
72
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { createProject, updateProject, listProjects, getInsights, getQuotes, getSolutions, getSolution, getPrototypes, getPrototype, exportPrototype } from './bifocalClient.js';
|
|
6
|
+
import { writeFile } from 'fs/promises';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
const server = new Server({ name: 'bifocal', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
9
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
10
|
+
tools: [
|
|
11
|
+
{
|
|
12
|
+
name: 'create_project',
|
|
13
|
+
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.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
name: { type: 'string', description: 'The name of the project.' },
|
|
18
|
+
description: { type: 'string', description: 'Optional PRD or context for the project.' },
|
|
19
|
+
},
|
|
20
|
+
required: ['name'],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'update_project',
|
|
25
|
+
description: 'Update a Bifocal project\'s name or description/context.',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
project_id: { type: 'string', description: 'The ID of the project to update.' },
|
|
30
|
+
name: { type: 'string', description: 'New name for the project.' },
|
|
31
|
+
description: { type: 'string', description: 'Updated PRD or context for the project.' },
|
|
32
|
+
},
|
|
33
|
+
required: ['project_id'],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'list_projects',
|
|
38
|
+
description: 'List all Bifocal projects for the authenticated user.',
|
|
39
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'get_insights',
|
|
43
|
+
description: 'Get all insights for a Bifocal project. Returns structured data including the user goal, problem, specific barriers, and mention count for each insight. Results are ordered by mention count descending.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
project_id: { type: 'string', description: 'The ID of the project to fetch insights for.' },
|
|
48
|
+
},
|
|
49
|
+
required: ['project_id'],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'get_quotes',
|
|
54
|
+
description: 'Get supporting user quotes for a specific insight. Call this when the user wants to see evidence or examples behind a particular insight.',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
project_id: { type: 'string', description: 'The ID of the project the insight belongs to.' },
|
|
59
|
+
insight_id: { type: 'string', description: 'The ID of the insight to fetch quotes for.' },
|
|
60
|
+
},
|
|
61
|
+
required: ['project_id', 'insight_id'],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'get_solutions',
|
|
66
|
+
description: 'Get all solutions for a Bifocal project. Returns title, brief, barriers addressed, and prototype link (if one was generated) for each solution.',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
project_id: { type: 'string', description: 'The ID of the project to fetch solutions for.' },
|
|
71
|
+
},
|
|
72
|
+
required: ['project_id'],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'get_solution',
|
|
77
|
+
description: 'Get full detail for a specific solution including all proposed interventions and page changes. Call this when the user wants to understand what a solution proposes to change.',
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
project_id: { type: 'string', description: 'The ID of the project the solution belongs to.' },
|
|
82
|
+
solution_id: { type: 'string', description: 'The ID of the solution to fetch.' },
|
|
83
|
+
},
|
|
84
|
+
required: ['project_id', 'solution_id'],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'get_prototypes',
|
|
89
|
+
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.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
project_id: { type: 'string', description: 'The ID of the project to fetch prototypes for.' },
|
|
94
|
+
},
|
|
95
|
+
required: ['project_id'],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'get_prototype',
|
|
100
|
+
description: 'Get full detail for a specific prototype including its sitemap and key capabilities. Use this when you need to understand what pages exist in a prototype.',
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
project_id: { type: 'string', description: 'The ID of the project the prototype belongs to.' },
|
|
105
|
+
prototype_id: { type: 'string', description: 'The ID of the prototype to fetch.' },
|
|
106
|
+
},
|
|
107
|
+
required: ['project_id', 'prototype_id'],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'export_prototype',
|
|
112
|
+
description: 'Download a prototype\'s source code as a ZIP file and save it to the local filesystem. Returns the path where the file was saved.',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
project_id: { type: 'string', description: 'The ID of the project the prototype belongs to.' },
|
|
117
|
+
prototype_id: { type: 'string', description: 'The ID of the prototype to export.' },
|
|
118
|
+
output_dir: { type: 'string', description: 'Directory to save the ZIP file. Defaults to the current working directory.' },
|
|
119
|
+
},
|
|
120
|
+
required: ['project_id', 'prototype_id'],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
}));
|
|
125
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
126
|
+
const { name, arguments: args } = request.params;
|
|
127
|
+
try {
|
|
128
|
+
if (name === 'update_project') {
|
|
129
|
+
const { project_id, name: projectName, description } = args;
|
|
130
|
+
const project = await updateProject(project_id, { name: projectName, description });
|
|
131
|
+
return { content: [{ type: 'text', text: JSON.stringify(project, null, 2) }] };
|
|
132
|
+
}
|
|
133
|
+
if (name === 'create_project') {
|
|
134
|
+
const { name: projectName } = args;
|
|
135
|
+
let description = args.description;
|
|
136
|
+
// Ask for PRD/context via elicitation if not already provided
|
|
137
|
+
if (!description) {
|
|
138
|
+
try {
|
|
139
|
+
const result = await server.elicitInput({
|
|
140
|
+
message: `Do you have a PRD or any context to add for "${projectName}"?`,
|
|
141
|
+
requestedSchema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
description: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
title: 'PRD / Context',
|
|
147
|
+
description: 'Paste a PRD, user research summary, or any relevant context for this project.',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
if (result.action === 'accept' && result.content?.description) {
|
|
153
|
+
description = result.content.description;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Client doesn't support elicitation — proceed without context
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const project = await createProject(projectName, description);
|
|
161
|
+
return { content: [{ type: 'text', text: JSON.stringify(project, null, 2) }] };
|
|
162
|
+
}
|
|
163
|
+
if (name === 'list_projects') {
|
|
164
|
+
const projects = await listProjects();
|
|
165
|
+
return { content: [{ type: 'text', text: JSON.stringify(projects, null, 2) }] };
|
|
166
|
+
}
|
|
167
|
+
if (name === 'get_insights') {
|
|
168
|
+
const { project_id } = args;
|
|
169
|
+
const insights = await getInsights(project_id);
|
|
170
|
+
return { content: [{ type: 'text', text: JSON.stringify(insights, null, 2) }] };
|
|
171
|
+
}
|
|
172
|
+
if (name === 'get_quotes') {
|
|
173
|
+
const { project_id, insight_id } = args;
|
|
174
|
+
const quotes = await getQuotes(project_id, insight_id);
|
|
175
|
+
return { content: [{ type: 'text', text: JSON.stringify(quotes, null, 2) }] };
|
|
176
|
+
}
|
|
177
|
+
if (name === 'get_solutions') {
|
|
178
|
+
const { project_id } = args;
|
|
179
|
+
const solutions = await getSolutions(project_id);
|
|
180
|
+
return { content: [{ type: 'text', text: JSON.stringify(solutions, null, 2) }] };
|
|
181
|
+
}
|
|
182
|
+
if (name === 'get_solution') {
|
|
183
|
+
const { project_id, solution_id } = args;
|
|
184
|
+
const solution = await getSolution(project_id, solution_id);
|
|
185
|
+
return { content: [{ type: 'text', text: JSON.stringify(solution, null, 2) }] };
|
|
186
|
+
}
|
|
187
|
+
if (name === 'get_prototypes') {
|
|
188
|
+
const { project_id } = args;
|
|
189
|
+
const prototypes = await getPrototypes(project_id);
|
|
190
|
+
return { content: [{ type: 'text', text: JSON.stringify(prototypes, null, 2) }] };
|
|
191
|
+
}
|
|
192
|
+
if (name === 'get_prototype') {
|
|
193
|
+
const { project_id, prototype_id } = args;
|
|
194
|
+
const prototype = await getPrototype(project_id, prototype_id);
|
|
195
|
+
return { content: [{ type: 'text', text: JSON.stringify(prototype, null, 2) }] };
|
|
196
|
+
}
|
|
197
|
+
if (name === 'export_prototype') {
|
|
198
|
+
const { project_id, prototype_id, output_dir } = args;
|
|
199
|
+
const { downloadUrl, fileName } = await exportPrototype(project_id, prototype_id);
|
|
200
|
+
const response = await fetch(downloadUrl);
|
|
201
|
+
if (!response.ok)
|
|
202
|
+
throw new Error(`Failed to download zip: ${response.status}`);
|
|
203
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
204
|
+
const dir = output_dir || process.cwd();
|
|
205
|
+
const filePath = join(dir, fileName);
|
|
206
|
+
await writeFile(filePath, buffer);
|
|
207
|
+
return { content: [{ type: 'text', text: `Saved to ${filePath}` }] };
|
|
208
|
+
}
|
|
209
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}` }],
|
|
214
|
+
isError: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
async function main() {
|
|
219
|
+
const transport = new StdioServerTransport();
|
|
220
|
+
await server.connect(transport);
|
|
221
|
+
}
|
|
222
|
+
main().catch((error) => {
|
|
223
|
+
console.error('MCP server error:', error);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bifocal/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bifocal MCP server — access projects, insights, and prototypes from Claude",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"bifocal-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsc --watch",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"prepublishOnly": "tsc"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^20.0.0",
|
|
24
|
+
"typescript": "^5.3.2"
|
|
25
|
+
}
|
|
26
|
+
}
|