blip-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/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/annotation-server.d.ts +7 -0
- package/dist/annotation-server.js +230 -0
- package/dist/dev-server.d.ts +1 -0
- package/dist/dev-server.js +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +77 -0
- package/package.json +51 -0
- package/public/app.js +320 -0
- package/public/html2canvas.min.js +20 -0
- package/public/index.html +60 -0
- package/public/logo.png +0 -0
- package/public/overlay.js +643 -0
- package/public/style.css +150 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ben Krämer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="logo.png" alt="Blip" width="200" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
Visual annotation MCP server for Claude Code.<br>
|
|
7
|
+
Draw on your preview. Claude sees what you mean.
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## How it works
|
|
13
|
+
|
|
14
|
+
1. Tell Claude: **"annotate"** (or **"annotate http://localhost:3000"**)
|
|
15
|
+
2. A browser opens with drawing tools on your page
|
|
16
|
+
3. Draw circles, arrows, highlights, text labels
|
|
17
|
+
4. Click **Send to Claude** -- the annotated screenshot goes back to your chat
|
|
18
|
+
5. Claude updates the code based on what you drew
|
|
19
|
+
|
|
20
|
+
No more describing UI changes with words. Just draw on them.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
claude mcp add blip -- npx blip-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That's it. Requires [Claude Code](https://claude.com/claude-code) and Node.js 18+.
|
|
29
|
+
|
|
30
|
+
## Two modes
|
|
31
|
+
|
|
32
|
+
**CLI (Terminal)** -- The `annotate` MCP tool opens your page in a browser with the overlay injected. Draw, hit send, the screenshot goes straight back to Claude.
|
|
33
|
+
|
|
34
|
+
**Desktop (Claude Code app)** -- The overlay works with Claude Code's built-in preview. Click the pencil button to activate drawing, then press Ctrl+P to add the screenshot to chat.
|
|
35
|
+
|
|
36
|
+
## Development
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/nebenzu/Blip.git
|
|
40
|
+
cd Blip
|
|
41
|
+
npm install
|
|
42
|
+
npm run build
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm run dev # Watch mode
|
|
47
|
+
npm run serve # Dev server on port 4460
|
|
48
|
+
npm start # Run MCP server directly
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface AnnotationResult {
|
|
2
|
+
imagePath: string;
|
|
3
|
+
}
|
|
4
|
+
export declare function startAnnotationServer(port?: number): Promise<number>;
|
|
5
|
+
export declare function waitForAnnotation(): Promise<AnnotationResult>;
|
|
6
|
+
export declare function getServerPort(): number;
|
|
7
|
+
export declare function stopAnnotationServer(): void;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { exec } from 'node:child_process';
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
const PROJECT_ROOT = resolve(__dirname, '..');
|
|
11
|
+
const PUBLIC_DIR = join(PROJECT_ROOT, 'public');
|
|
12
|
+
const SCREENSHOTS_DIR = join(PROJECT_ROOT, 'screenshots');
|
|
13
|
+
const LANDING_DIR = join(PROJECT_ROOT, 'landing');
|
|
14
|
+
let pendingResolve = null;
|
|
15
|
+
let server = null;
|
|
16
|
+
let serverPort = 0;
|
|
17
|
+
let proxyTarget = null;
|
|
18
|
+
export async function startAnnotationServer(port = 4461) {
|
|
19
|
+
if (server)
|
|
20
|
+
return serverPort;
|
|
21
|
+
await mkdir(SCREENSHOTS_DIR, { recursive: true });
|
|
22
|
+
const app = express();
|
|
23
|
+
// Serve landing page at root
|
|
24
|
+
app.get('/', (_req, res) => {
|
|
25
|
+
const landingIndex = join(LANDING_DIR, 'index.html');
|
|
26
|
+
if (existsSync(landingIndex)) {
|
|
27
|
+
res.sendFile(landingIndex);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
res.redirect('/annotate');
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
// Serve landing page static files
|
|
34
|
+
app.use('/landing', express.static(LANDING_DIR));
|
|
35
|
+
// Proxy route: fetch any URL and inject the overlay script
|
|
36
|
+
app.get('/proxy', async (req, res) => {
|
|
37
|
+
const targetUrl = req.query.url;
|
|
38
|
+
if (!targetUrl) {
|
|
39
|
+
res.status(400).send('Missing ?url= parameter');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
// Store target origin for catch-all proxy
|
|
44
|
+
const parsedTarget = new URL(targetUrl);
|
|
45
|
+
proxyTarget = parsedTarget.origin;
|
|
46
|
+
const response = await fetch(targetUrl);
|
|
47
|
+
let html = await response.text();
|
|
48
|
+
const contentType = response.headers.get('content-type') || 'text/html';
|
|
49
|
+
if (contentType.includes('text/html')) {
|
|
50
|
+
// Inject overlay script before </body> (use absolute URL to avoid <base> tag redirect)
|
|
51
|
+
const overlayScript = `<script src="http://localhost:${serverPort}/overlay.js?v=${Date.now()}"></script>`;
|
|
52
|
+
if (html.includes('</body>')) {
|
|
53
|
+
html = html.replace('</body>', `${overlayScript}\n</body>`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
html += overlayScript;
|
|
57
|
+
}
|
|
58
|
+
// Add <base> so the app's own assets and API calls resolve to the target origin
|
|
59
|
+
const url = new URL(targetUrl);
|
|
60
|
+
const base = `<base href="${url.origin}/">`;
|
|
61
|
+
if (html.includes('<head>')) {
|
|
62
|
+
html = html.replace('<head>', `<head>\n${base}`);
|
|
63
|
+
}
|
|
64
|
+
else if (html.includes('<html')) {
|
|
65
|
+
html = html.replace(/<html[^>]*>/, `$&\n<head>${base}</head>`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
res.type('text/html').send(html);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
res.status(500).send(`Failed to fetch ${targetUrl}`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// Annotation app at /annotate
|
|
75
|
+
app.get('/annotate', (_req, res) => {
|
|
76
|
+
res.sendFile(join(PUBLIC_DIR, 'index.html'));
|
|
77
|
+
});
|
|
78
|
+
// Static files for annotation app
|
|
79
|
+
app.use(express.static(PUBLIC_DIR, { etag: false, lastModified: false, maxAge: 0 }));
|
|
80
|
+
// Serve an image by path (restricted to screenshots directory)
|
|
81
|
+
app.get('/api/image', (req, res) => {
|
|
82
|
+
const imgPath = req.query.path;
|
|
83
|
+
if (!imgPath)
|
|
84
|
+
return res.status(400).json({ error: 'No path provided' });
|
|
85
|
+
const resolved = resolve(imgPath);
|
|
86
|
+
if (!resolved.startsWith(SCREENSHOTS_DIR)) {
|
|
87
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
88
|
+
}
|
|
89
|
+
if (!existsSync(resolved))
|
|
90
|
+
return res.status(404).json({ error: 'Image not found' });
|
|
91
|
+
res.sendFile(resolved);
|
|
92
|
+
});
|
|
93
|
+
// Save annotated image
|
|
94
|
+
app.post('/api/save', async (req, res) => {
|
|
95
|
+
try {
|
|
96
|
+
const chunks = [];
|
|
97
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
98
|
+
await new Promise((resolve) => req.on('end', resolve));
|
|
99
|
+
const body = Buffer.concat(chunks);
|
|
100
|
+
const boundary = req.headers['content-type']?.split('boundary=')[1];
|
|
101
|
+
let imageBuffer;
|
|
102
|
+
if (boundary) {
|
|
103
|
+
const bodyStr = body.toString('latin1');
|
|
104
|
+
const parts = bodyStr.split('--' + boundary);
|
|
105
|
+
const imagePart = parts.find(p => p.includes('filename='));
|
|
106
|
+
if (!imagePart) {
|
|
107
|
+
res.status(400).json({ error: 'No image in request' });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const headerEnd = imagePart.indexOf('\r\n\r\n');
|
|
111
|
+
const dataStart = headerEnd + 4;
|
|
112
|
+
const dataEnd = imagePart.lastIndexOf('\r\n');
|
|
113
|
+
imageBuffer = Buffer.from(imagePart.slice(dataStart, dataEnd), 'latin1');
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
imageBuffer = body;
|
|
117
|
+
}
|
|
118
|
+
const filename = `annotated-${Date.now()}.png`;
|
|
119
|
+
const filepath = join(SCREENSHOTS_DIR, filename);
|
|
120
|
+
await writeFile(filepath, imageBuffer);
|
|
121
|
+
if (pendingResolve) {
|
|
122
|
+
pendingResolve({ imagePath: filepath });
|
|
123
|
+
pendingResolve = null;
|
|
124
|
+
}
|
|
125
|
+
res.json({ success: true, path: filepath });
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
// Save error
|
|
129
|
+
res.status(500).json({ error: 'Failed to save' });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
// Save annotated image from base64 JSON
|
|
133
|
+
app.post('/api/save-base64', express.json({ limit: '50mb' }), async (req, res) => {
|
|
134
|
+
try {
|
|
135
|
+
const { image, ...metadata } = req.body;
|
|
136
|
+
if (!image) {
|
|
137
|
+
res.status(400).json({ error: 'No image data' });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const imageBuffer = Buffer.from(image, 'base64');
|
|
141
|
+
const filename = `annotated-${Date.now()}.png`;
|
|
142
|
+
const filepath = join(SCREENSHOTS_DIR, filename);
|
|
143
|
+
await writeFile(filepath, imageBuffer);
|
|
144
|
+
// Write all metadata alongside
|
|
145
|
+
const metaPath = filepath.replace('.png', '.json');
|
|
146
|
+
await writeFile(metaPath, JSON.stringify({
|
|
147
|
+
...metadata,
|
|
148
|
+
timestamp: new Date().toISOString()
|
|
149
|
+
}, null, 2));
|
|
150
|
+
// Copy image to clipboard on macOS so user can Cmd+V in chat
|
|
151
|
+
if (process.platform === 'darwin') {
|
|
152
|
+
const safePath = filepath.replace(/'/g, "'\\''");
|
|
153
|
+
exec(`osascript -e 'set the clipboard to (read (POSIX file "${safePath}") as «class PNGf»)'`, (err) => {
|
|
154
|
+
if (err)
|
|
155
|
+
console.error('Clipboard copy failed:', err);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (pendingResolve) {
|
|
159
|
+
pendingResolve({ imagePath: filepath });
|
|
160
|
+
pendingResolve = null;
|
|
161
|
+
}
|
|
162
|
+
res.json({ success: true, path: filepath });
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
// Save error
|
|
166
|
+
res.status(500).json({ error: 'Failed to save' });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
// Catch-all: proxy unknown requests to the target server
|
|
170
|
+
app.use(async (req, res) => {
|
|
171
|
+
if (!proxyTarget) {
|
|
172
|
+
res.status(404).send('Not found');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const targetUrl = `${proxyTarget}${req.originalUrl}`;
|
|
177
|
+
const headers = {};
|
|
178
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
179
|
+
if (typeof value === 'string' && key !== 'host')
|
|
180
|
+
headers[key] = value;
|
|
181
|
+
}
|
|
182
|
+
const fetchRes = await fetch(targetUrl, {
|
|
183
|
+
method: req.method,
|
|
184
|
+
headers,
|
|
185
|
+
body: ['GET', 'HEAD'].includes(req.method) ? undefined : req.body,
|
|
186
|
+
});
|
|
187
|
+
res.status(fetchRes.status);
|
|
188
|
+
fetchRes.headers.forEach((value, key) => {
|
|
189
|
+
if (!['transfer-encoding', 'content-encoding', 'connection'].includes(key.toLowerCase())) {
|
|
190
|
+
res.setHeader(key, value);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
const buffer = Buffer.from(await fetchRes.arrayBuffer());
|
|
194
|
+
res.send(buffer);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
res.status(502).send('Proxy error');
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
server = createServer(app);
|
|
202
|
+
server.listen(port, () => {
|
|
203
|
+
serverPort = port;
|
|
204
|
+
resolve(port);
|
|
205
|
+
});
|
|
206
|
+
server.on('error', () => {
|
|
207
|
+
server = createServer(app);
|
|
208
|
+
server.listen(0, () => {
|
|
209
|
+
const addr = server.address();
|
|
210
|
+
serverPort = typeof addr === 'object' && addr ? addr.port : port;
|
|
211
|
+
resolve(serverPort);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
export function waitForAnnotation() {
|
|
217
|
+
return new Promise((resolve) => {
|
|
218
|
+
pendingResolve = resolve;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
export function getServerPort() {
|
|
222
|
+
return serverPort;
|
|
223
|
+
}
|
|
224
|
+
export function stopAnnotationServer() {
|
|
225
|
+
if (server) {
|
|
226
|
+
server.close();
|
|
227
|
+
server = null;
|
|
228
|
+
serverPort = 0;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Standalone dev server for previewing the annotation UI
|
|
2
|
+
import { startAnnotationServer } from './annotation-server.js';
|
|
3
|
+
const port = parseInt(process.argv[2] || '4460');
|
|
4
|
+
startAnnotationServer(port).then((actualPort) => {
|
|
5
|
+
console.log(`Blip server running at http://localhost:${actualPort}`);
|
|
6
|
+
console.log(` Annotation editor: http://localhost:${actualPort}/annotate`);
|
|
7
|
+
}).catch((err) => {
|
|
8
|
+
console.error('Failed to start server:', err);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { startAnnotationServer, waitForAnnotation } from './annotation-server.js';
|
|
6
|
+
import { exec } from 'node:child_process';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = import.meta.dirname ?? join(__filename, '..');
|
|
13
|
+
function openBrowser(url) {
|
|
14
|
+
const cmd = process.platform === 'darwin' ? 'open' :
|
|
15
|
+
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
16
|
+
const child = exec(`${cmd} "${url}"`);
|
|
17
|
+
// Prevent child process output from corrupting MCP stdio transport
|
|
18
|
+
child.stdout?.destroy();
|
|
19
|
+
child.stderr?.destroy();
|
|
20
|
+
}
|
|
21
|
+
// Read image + metadata, return MCP content array with inline image
|
|
22
|
+
async function buildAnnotationResponse(imagePath, extra = {}) {
|
|
23
|
+
const imageData = await readFile(imagePath);
|
|
24
|
+
const base64 = imageData.toString('base64');
|
|
25
|
+
let metadata = {};
|
|
26
|
+
const metaPath = imagePath.replace('.png', '.json');
|
|
27
|
+
try {
|
|
28
|
+
if (existsSync(metaPath)) {
|
|
29
|
+
metadata = JSON.parse(await readFile(metaPath, 'utf-8'));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch { }
|
|
33
|
+
return {
|
|
34
|
+
content: [
|
|
35
|
+
{
|
|
36
|
+
type: 'image',
|
|
37
|
+
data: base64,
|
|
38
|
+
mimeType: 'image/png',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: 'text',
|
|
42
|
+
text: JSON.stringify({
|
|
43
|
+
annotated_image_path: imagePath,
|
|
44
|
+
metadata,
|
|
45
|
+
...extra,
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const server = new McpServer({
|
|
52
|
+
name: 'blip',
|
|
53
|
+
version: '0.1.0',
|
|
54
|
+
});
|
|
55
|
+
// Single tool: annotate a live page or open the standalone editor
|
|
56
|
+
server.tool('annotate', 'Open a visual annotation editor. The user can draw circles, arrows, highlights, and text on a screenshot. Returns the annotated image directly.', {
|
|
57
|
+
url: z.string().optional().describe('URL of a live page to annotate (e.g., http://localhost:3000). If omitted, opens the standalone editor where you can paste or drop a screenshot.'),
|
|
58
|
+
}, async ({ url: targetUrl }) => {
|
|
59
|
+
const port = await startAnnotationServer();
|
|
60
|
+
let browserUrl;
|
|
61
|
+
if (targetUrl) {
|
|
62
|
+
// Proxy mode: inject overlay on the live page
|
|
63
|
+
browserUrl = `http://localhost:${port}/proxy?url=${encodeURIComponent(targetUrl)}`;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// Standalone editor mode
|
|
67
|
+
browserUrl = `http://localhost:${port}/annotate`;
|
|
68
|
+
}
|
|
69
|
+
openBrowser(browserUrl);
|
|
70
|
+
const result = await waitForAnnotation();
|
|
71
|
+
return buildAnnotationResponse(result.imagePath, targetUrl ? { source_url: targetUrl } : {});
|
|
72
|
+
});
|
|
73
|
+
async function main() {
|
|
74
|
+
const transport = new StdioServerTransport();
|
|
75
|
+
await server.connect(transport);
|
|
76
|
+
}
|
|
77
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blip-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Visual annotation MCP server for Claude Code - draw on preview screenshots to communicate UI changes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"blip": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"serve": "node dist/dev-server.js",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"blip",
|
|
20
|
+
"claude",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"annotation",
|
|
23
|
+
"screenshot",
|
|
24
|
+
"visual-feedback",
|
|
25
|
+
"model-context-protocol",
|
|
26
|
+
"ai-coding"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/nebenzu/Blip"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"public",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
44
|
+
"express": "^5.1.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/express": "^5.0.2",
|
|
48
|
+
"@types/node": "^22.15.3",
|
|
49
|
+
"typescript": "^5.8.3"
|
|
50
|
+
}
|
|
51
|
+
}
|