viewpo-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 +180 -0
- package/dist/client.d.ts +18 -0
- package/dist/client.js +71 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +173 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Little Bear Apps
|
|
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,180 @@
|
|
|
1
|
+
# viewpo-mcp
|
|
2
|
+
|
|
3
|
+
MCP server that gives AI coding assistants **eyes into responsive viewport rendering** via the [Viewpo](https://viewpo.app) macOS app.
|
|
4
|
+
|
|
5
|
+
Capture multi-viewport screenshots, extract DOM layout trees, and compare responsive behaviour across breakpoints — all from your AI assistant.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
AI coding assistants (Claude Code, Cursor, Windsurf, etc.) are **blind** when working on frontend UI. They write CSS and HTML but can't see the visual result. This creates a slow feedback loop:
|
|
10
|
+
|
|
11
|
+
> AI writes code → you check → you describe what's wrong → AI fixes → repeat
|
|
12
|
+
|
|
13
|
+
**viewpo-mcp** closes that loop. The AI can now:
|
|
14
|
+
|
|
15
|
+
1. **Screenshot** a URL at multiple viewport widths simultaneously
|
|
16
|
+
2. **Extract** the DOM layout tree with bounding rects and computed styles
|
|
17
|
+
3. **Compare** layouts at two different viewport widths to find responsive issues
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- **[Viewpo](https://viewpo.app) macOS app** with MCP Bridge enabled (Settings → MCP Bridge → Start)
|
|
22
|
+
- **Node.js 20+**
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
### 1. Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g viewpo-mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or run directly with npx (no install needed):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx viewpo-mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Enable the MCP Bridge in Viewpo
|
|
39
|
+
|
|
40
|
+
1. Open Viewpo on your Mac
|
|
41
|
+
2. Go to **Settings → MCP Bridge**
|
|
42
|
+
3. Click **Start** to enable the bridge server
|
|
43
|
+
4. Copy the **auth token** (click the copy button next to the token)
|
|
44
|
+
|
|
45
|
+
### 3. Configure your AI assistant
|
|
46
|
+
|
|
47
|
+
#### Claude Code
|
|
48
|
+
|
|
49
|
+
Add to your MCP config (`~/.claude/mcp.json` or project `.mcp.json`):
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"viewpo": {
|
|
55
|
+
"command": "npx",
|
|
56
|
+
"args": ["-y", "viewpo-mcp"],
|
|
57
|
+
"env": {
|
|
58
|
+
"VIEWPO_AUTH_TOKEN": "<paste token from step 2>"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### Cursor
|
|
66
|
+
|
|
67
|
+
Add to your Cursor MCP settings (`.cursor/mcp.json`):
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"mcpServers": {
|
|
72
|
+
"viewpo": {
|
|
73
|
+
"command": "npx",
|
|
74
|
+
"args": ["-y", "viewpo-mcp"],
|
|
75
|
+
"env": {
|
|
76
|
+
"VIEWPO_AUTH_TOKEN": "<paste token from step 2>"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### Other MCP-compatible assistants
|
|
84
|
+
|
|
85
|
+
Any assistant supporting the [Model Context Protocol](https://modelcontextprotocol.io) can use viewpo-mcp. Set the command to `npx -y viewpo-mcp` and provide the `VIEWPO_AUTH_TOKEN` environment variable.
|
|
86
|
+
|
|
87
|
+
## Tools
|
|
88
|
+
|
|
89
|
+
### `viewpo_screenshot`
|
|
90
|
+
|
|
91
|
+
Capture screenshots of a URL at one or more viewport widths. Returns base64 JPEG images.
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
url: "https://example.com" (required)
|
|
95
|
+
viewports: [{ width: 375, name: "phone" }, (optional, defaults to 1920px desktop)
|
|
96
|
+
{ width: 1920, name: "desktop" }]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Common widths: **375** (phone), **820** (tablet), **1920** (desktop).
|
|
100
|
+
|
|
101
|
+
### `viewpo_get_layout_map`
|
|
102
|
+
|
|
103
|
+
Extract the DOM layout tree at a given viewport width. Returns element hierarchy with tags, classes, bounding rects, and computed CSS styles.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
url: "https://example.com" (required)
|
|
107
|
+
viewport: 1920 (optional, default 1920)
|
|
108
|
+
selector: ".main-content" (optional, scope to subtree)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `viewpo_compare_viewports`
|
|
112
|
+
|
|
113
|
+
Compare the layout of a URL at two different viewport widths. Returns elements whose size or CSS styles differ between the two viewports.
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
url: "https://example.com" (required)
|
|
117
|
+
viewport_a: 375 (required)
|
|
118
|
+
viewport_b: 1920 (required)
|
|
119
|
+
selector: ".hero" (optional, scope to subtree)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Environment variables
|
|
123
|
+
|
|
124
|
+
| Variable | Required | Default | Description |
|
|
125
|
+
|----------|----------|---------|-------------|
|
|
126
|
+
| `VIEWPO_AUTH_TOKEN` | Yes | — | Bearer token from Viewpo Settings → MCP Bridge |
|
|
127
|
+
| `VIEWPO_PORT` | No | `9847` | Port the Viewpo bridge server listens on |
|
|
128
|
+
|
|
129
|
+
## How it works
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
AI Assistant (Claude Code / Cursor / Windsurf)
|
|
133
|
+
| stdio (MCP protocol)
|
|
134
|
+
v
|
|
135
|
+
viewpo-mcp (this package)
|
|
136
|
+
| HTTP localhost:9847
|
|
137
|
+
v
|
|
138
|
+
Viewpo macOS app (NWListener)
|
|
139
|
+
| WKWebView rendering
|
|
140
|
+
v
|
|
141
|
+
Headless browser pool (off-screen, up to 4 concurrent pages)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The Viewpo app runs a local HTTP server on `localhost:9847`. This MCP server translates tool calls into HTTP requests to that bridge. The app loads pages in headless WKWebView instances with real CSS viewport simulation — media queries fire at the target width, not the physical screen width.
|
|
145
|
+
|
|
146
|
+
**Key advantage**: WKWebView bypasses X-Frame-Options entirely. Any website loads, regardless of security headers that would block iframe-based tools.
|
|
147
|
+
|
|
148
|
+
## Example workflow
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
You: "The hero section looks broken on mobile"
|
|
152
|
+
|
|
153
|
+
AI: 1. viewpo_screenshot("https://preview.example.com", [{width: 375}, {width: 1920}])
|
|
154
|
+
→ Sees the layout at both sizes
|
|
155
|
+
2. viewpo_compare_viewports("https://preview.example.com", 375, 1920)
|
|
156
|
+
→ Gets structured diff: ".hero img" is 1200px wide on mobile (overflowing)
|
|
157
|
+
3. Fixes the CSS
|
|
158
|
+
4. viewpo_screenshot("https://preview.example.com", [{width: 375}])
|
|
159
|
+
→ Verifies the fix
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
git clone https://github.com/littlebearapps/viewpo-mcp.git
|
|
166
|
+
cd viewpo-mcp
|
|
167
|
+
npm install
|
|
168
|
+
npm run build # compile TypeScript
|
|
169
|
+
npm run dev # watch mode
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Licence
|
|
173
|
+
|
|
174
|
+
MIT - see [LICENSE](LICENSE).
|
|
175
|
+
|
|
176
|
+
## Links
|
|
177
|
+
|
|
178
|
+
- [Viewpo app](https://viewpo.app) - The macOS/iOS app
|
|
179
|
+
- [Model Context Protocol](https://modelcontextprotocol.io) - MCP specification
|
|
180
|
+
- [GitHub](https://github.com/littlebearapps/viewpo-mcp) - Source code
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the Viewpo bridge API.
|
|
3
|
+
* Communicates with the Viewpo macOS app's NWListener on localhost.
|
|
4
|
+
*/
|
|
5
|
+
import type { ScreenshotRequest, ScreenshotResponse, LayoutMapRequest, LayoutMapResponse, CompareRequest, CompareResponse, HealthResponse } from "./types.js";
|
|
6
|
+
export declare class ViewpoBridgeClient {
|
|
7
|
+
private readonly baseURL;
|
|
8
|
+
private readonly authToken;
|
|
9
|
+
constructor(port?: number, authToken?: string);
|
|
10
|
+
health(): Promise<HealthResponse>;
|
|
11
|
+
screenshot(request: ScreenshotRequest): Promise<ScreenshotResponse>;
|
|
12
|
+
layoutMap(request: LayoutMapRequest): Promise<LayoutMapResponse>;
|
|
13
|
+
compare(request: CompareRequest): Promise<CompareResponse>;
|
|
14
|
+
private get;
|
|
15
|
+
private post;
|
|
16
|
+
private headers;
|
|
17
|
+
private handleResponse;
|
|
18
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the Viewpo bridge API.
|
|
3
|
+
* Communicates with the Viewpo macOS app's NWListener on localhost.
|
|
4
|
+
*/
|
|
5
|
+
export class ViewpoBridgeClient {
|
|
6
|
+
baseURL;
|
|
7
|
+
authToken;
|
|
8
|
+
constructor(port = 9847, authToken = "") {
|
|
9
|
+
this.baseURL = `http://127.0.0.1:${port}`;
|
|
10
|
+
this.authToken = authToken;
|
|
11
|
+
}
|
|
12
|
+
async health() {
|
|
13
|
+
return this.get("/health");
|
|
14
|
+
}
|
|
15
|
+
async screenshot(request) {
|
|
16
|
+
return this.post("/screenshot", request);
|
|
17
|
+
}
|
|
18
|
+
async layoutMap(request) {
|
|
19
|
+
return this.post("/layout", request);
|
|
20
|
+
}
|
|
21
|
+
async compare(request) {
|
|
22
|
+
return this.post("/compare", request);
|
|
23
|
+
}
|
|
24
|
+
async get(path) {
|
|
25
|
+
const response = await fetch(`${this.baseURL}${path}`, {
|
|
26
|
+
method: "GET",
|
|
27
|
+
headers: this.headers(false),
|
|
28
|
+
});
|
|
29
|
+
return this.handleResponse(response);
|
|
30
|
+
}
|
|
31
|
+
async post(path, body) {
|
|
32
|
+
const response = await fetch(`${this.baseURL}${path}`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: this.headers(true),
|
|
35
|
+
body: JSON.stringify(body),
|
|
36
|
+
});
|
|
37
|
+
return this.handleResponse(response);
|
|
38
|
+
}
|
|
39
|
+
headers(includeContentType) {
|
|
40
|
+
const h = {};
|
|
41
|
+
if (this.authToken) {
|
|
42
|
+
h["Authorization"] = `Bearer ${this.authToken}`;
|
|
43
|
+
}
|
|
44
|
+
if (includeContentType) {
|
|
45
|
+
h["Content-Type"] = "application/json";
|
|
46
|
+
}
|
|
47
|
+
return h;
|
|
48
|
+
}
|
|
49
|
+
async handleResponse(response) {
|
|
50
|
+
const text = await response.text();
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
let message = `HTTP ${response.status}`;
|
|
53
|
+
try {
|
|
54
|
+
const err = JSON.parse(text);
|
|
55
|
+
message = err.error || message;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
if (text)
|
|
59
|
+
message = text;
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`Viewpo bridge error (${response.status}): ${message}`);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(text);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new Error(`Invalid JSON response from Viewpo bridge: ${text.slice(0, 200)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Viewpo MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Gives AI coding assistants visual inspection tools for responsive
|
|
6
|
+
* web development via the Viewpo macOS app.
|
|
7
|
+
*
|
|
8
|
+
* Transport: stdio (for Claude Code, Cursor, Windsurf, etc.)
|
|
9
|
+
* Communicates with: Viewpo macOS app on localhost:9847
|
|
10
|
+
*
|
|
11
|
+
* @see https://github.com/littlebearapps/viewpo-mcp
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Viewpo MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Gives AI coding assistants visual inspection tools for responsive
|
|
6
|
+
* web development via the Viewpo macOS app.
|
|
7
|
+
*
|
|
8
|
+
* Transport: stdio (for Claude Code, Cursor, Windsurf, etc.)
|
|
9
|
+
* Communicates with: Viewpo macOS app on localhost:9847
|
|
10
|
+
*
|
|
11
|
+
* @see https://github.com/littlebearapps/viewpo-mcp
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { ViewpoBridgeClient } from "./client.js";
|
|
17
|
+
// --- Configuration from environment ---
|
|
18
|
+
const port = parseInt(process.env.VIEWPO_PORT || "9847", 10);
|
|
19
|
+
const authToken = process.env.VIEWPO_AUTH_TOKEN || "";
|
|
20
|
+
if (!authToken) {
|
|
21
|
+
console.error("Warning: VIEWPO_AUTH_TOKEN not set. Copy the token from Viewpo > Settings > MCP Bridge.");
|
|
22
|
+
}
|
|
23
|
+
const client = new ViewpoBridgeClient(port, authToken);
|
|
24
|
+
// --- MCP Server ---
|
|
25
|
+
const server = new McpServer({
|
|
26
|
+
name: "viewpo",
|
|
27
|
+
version: "0.1.0",
|
|
28
|
+
});
|
|
29
|
+
// --- Tool: viewpo_screenshot ---
|
|
30
|
+
server.tool("viewpo_screenshot", "Capture screenshots of a URL at one or more viewport widths. Returns base64 JPEG images. Use this to SEE what a webpage looks like at different screen sizes.", {
|
|
31
|
+
url: z.string().url().describe("The URL to screenshot"),
|
|
32
|
+
viewports: z
|
|
33
|
+
.array(z.object({
|
|
34
|
+
width: z
|
|
35
|
+
.number()
|
|
36
|
+
.int()
|
|
37
|
+
.min(100)
|
|
38
|
+
.max(3840)
|
|
39
|
+
.describe("Viewport width in CSS pixels"),
|
|
40
|
+
name: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Label for this viewport (e.g. "phone", "desktop")'),
|
|
44
|
+
}))
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Viewports to capture. Defaults to desktop (1920px) if omitted. Common widths: 375 (phone), 820 (tablet), 1920 (desktop)."),
|
|
47
|
+
}, async ({ url, viewports }) => {
|
|
48
|
+
try {
|
|
49
|
+
const result = await client.screenshot({ url, viewports });
|
|
50
|
+
const content = result.viewports.flatMap((vp) => [
|
|
51
|
+
{
|
|
52
|
+
type: "text",
|
|
53
|
+
text: `**${vp.viewport}** (${vp.width}\u00d7${vp.height})`,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: "image",
|
|
57
|
+
data: vp.image_base64,
|
|
58
|
+
mimeType: "image/jpeg",
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
return { content };
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
content: [
|
|
66
|
+
{
|
|
67
|
+
type: "text",
|
|
68
|
+
text: `Screenshot failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
isError: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
// --- Tool: viewpo_get_layout_map ---
|
|
76
|
+
server.tool("viewpo_get_layout_map", "Extract the DOM layout tree of a URL at a given viewport width. Returns element hierarchy with tags, classes, bounding rects, and computed CSS styles. Use this to understand page structure and find layout issues.", {
|
|
77
|
+
url: z.string().url().describe("The URL to inspect"),
|
|
78
|
+
viewport: z
|
|
79
|
+
.number()
|
|
80
|
+
.int()
|
|
81
|
+
.min(100)
|
|
82
|
+
.max(3840)
|
|
83
|
+
.optional()
|
|
84
|
+
.describe("Viewport width in CSS pixels (default: 1920)"),
|
|
85
|
+
selector: z
|
|
86
|
+
.string()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('CSS selector to scope the layout map to a subtree (e.g. ".main-content", "#hero")'),
|
|
89
|
+
}, async ({ url, viewport, selector }) => {
|
|
90
|
+
try {
|
|
91
|
+
const result = await client.layoutMap({ url, viewport, selector });
|
|
92
|
+
return {
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: "text",
|
|
96
|
+
text: JSON.stringify(result, null, 2),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: `Layout map failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
isError: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
// --- Tool: viewpo_compare_viewports ---
|
|
114
|
+
server.tool("viewpo_compare_viewports", "Compare the layout of a URL at two different viewport widths. Returns a list of elements whose size or CSS styles differ between the two viewports. Use this to find responsive design issues.", {
|
|
115
|
+
url: z.string().url().describe("The URL to compare"),
|
|
116
|
+
viewport_a: z
|
|
117
|
+
.number()
|
|
118
|
+
.int()
|
|
119
|
+
.min(100)
|
|
120
|
+
.max(3840)
|
|
121
|
+
.describe("First viewport width in CSS pixels (e.g. 375 for phone)"),
|
|
122
|
+
viewport_b: z
|
|
123
|
+
.number()
|
|
124
|
+
.int()
|
|
125
|
+
.min(100)
|
|
126
|
+
.max(3840)
|
|
127
|
+
.describe("Second viewport width in CSS pixels (e.g. 1920 for desktop)"),
|
|
128
|
+
selector: z
|
|
129
|
+
.string()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe("CSS selector to scope comparison to a subtree"),
|
|
132
|
+
}, async ({ url, viewport_a, viewport_b, selector }) => {
|
|
133
|
+
try {
|
|
134
|
+
const result = await client.compare({
|
|
135
|
+
url,
|
|
136
|
+
viewport_a,
|
|
137
|
+
viewport_b,
|
|
138
|
+
selector,
|
|
139
|
+
});
|
|
140
|
+
const summary = result.differences.length === 0
|
|
141
|
+
? "No layout differences found between the two viewports."
|
|
142
|
+
: `Found ${result.differences.length} difference(s) between ${result.viewport_a}px and ${result.viewport_b}px:`;
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: "text",
|
|
147
|
+
text: `${summary}\n\n${JSON.stringify(result, null, 2)}`,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: "text",
|
|
157
|
+
text: `Compare failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
isError: true,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
// --- Start ---
|
|
165
|
+
async function main() {
|
|
166
|
+
const transport = new StdioServerTransport();
|
|
167
|
+
await server.connect(transport);
|
|
168
|
+
}
|
|
169
|
+
main().catch((error) => {
|
|
170
|
+
console.error("Viewpo MCP server fatal error:", error);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
});
|
|
173
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** TypeScript interfaces matching the Viewpo macOS app bridge API. */
|
|
2
|
+
export interface ViewportSpec {
|
|
3
|
+
width: number;
|
|
4
|
+
name?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ScreenshotRequest {
|
|
7
|
+
url: string;
|
|
8
|
+
viewports?: ViewportSpec[];
|
|
9
|
+
}
|
|
10
|
+
export interface ViewportResult {
|
|
11
|
+
viewport: string;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
image_base64: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ScreenshotResponse {
|
|
17
|
+
viewports: ViewportResult[];
|
|
18
|
+
}
|
|
19
|
+
export interface LayoutMapRequest {
|
|
20
|
+
url: string;
|
|
21
|
+
viewport?: number;
|
|
22
|
+
selector?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ElementRect {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
}
|
|
30
|
+
export interface LayoutElement {
|
|
31
|
+
tag: string;
|
|
32
|
+
id?: string;
|
|
33
|
+
classes?: string[];
|
|
34
|
+
rect: ElementRect;
|
|
35
|
+
styles: Record<string, string>;
|
|
36
|
+
children: LayoutElement[];
|
|
37
|
+
}
|
|
38
|
+
export interface LayoutMapResponse {
|
|
39
|
+
url: string;
|
|
40
|
+
viewport: number;
|
|
41
|
+
root: LayoutElement;
|
|
42
|
+
}
|
|
43
|
+
export interface CompareRequest {
|
|
44
|
+
url: string;
|
|
45
|
+
viewport_a: number;
|
|
46
|
+
viewport_b: number;
|
|
47
|
+
selector?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface LayoutDifference {
|
|
50
|
+
selector: string;
|
|
51
|
+
tag: string;
|
|
52
|
+
property: string;
|
|
53
|
+
value_a: string;
|
|
54
|
+
value_b: string;
|
|
55
|
+
}
|
|
56
|
+
export interface CompareResponse {
|
|
57
|
+
url: string;
|
|
58
|
+
viewport_a: number;
|
|
59
|
+
viewport_b: number;
|
|
60
|
+
differences: LayoutDifference[];
|
|
61
|
+
}
|
|
62
|
+
export interface HealthResponse {
|
|
63
|
+
status: string;
|
|
64
|
+
version: string;
|
|
65
|
+
port: number;
|
|
66
|
+
}
|
|
67
|
+
export interface ErrorResponse {
|
|
68
|
+
error: string;
|
|
69
|
+
code?: number;
|
|
70
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,sEAAsE"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "viewpo-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Viewpo — gives AI coding assistants eyes into responsive viewport rendering",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"viewpo-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"model-context-protocol",
|
|
19
|
+
"viewpo",
|
|
20
|
+
"viewport",
|
|
21
|
+
"responsive",
|
|
22
|
+
"screenshot",
|
|
23
|
+
"ai",
|
|
24
|
+
"claude",
|
|
25
|
+
"cursor",
|
|
26
|
+
"webview",
|
|
27
|
+
"frontend",
|
|
28
|
+
"testing"
|
|
29
|
+
],
|
|
30
|
+
"author": "Little Bear Apps <hello@littlebearapps.com> (https://littlebearapps.com)",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/littlebearapps/viewpo-mcp.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/littlebearapps/viewpo-mcp/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/littlebearapps/viewpo-mcp#readme",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
42
|
+
"zod": "^3.22.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"typescript": "^5.7.0",
|
|
46
|
+
"@types/node": "^22.0.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"dist",
|
|
53
|
+
"LICENSE",
|
|
54
|
+
"README.md"
|
|
55
|
+
]
|
|
56
|
+
}
|