pursr 0.8.0 → 0.8.1

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -4
  3. package/src/mcp.js +60 -125
package/README.md CHANGED
@@ -33,7 +33,7 @@
33
33
  Most teams need **five separate tools** to do visual QA: a screenshot CLI, a regression diff runner, an accessibility auditor, a way to share captures with an AI assistant, and a way to **turn all of that into a PDF report** for stakeholders. **pursr is all five** - built as a single Node.js package with:
34
34
 
35
35
  - **A unified CLI** (`pursr`) for every capture, diff, sweep, and audit.
36
- - **An agent-grade MCP stdio server** (`pursr-mcp`) with persistent tabs, direct image responses, rendered-state inspection, actions, diagnostics, screenshots, sweeps, and resources.
36
+ - **An agent-grade MCP stdio server** (`pursr-mcp`) built on the official Model Context Protocol SDK, with persistent tabs, direct image responses, rendered-state inspection, actions, diagnostics, screenshots, sweeps, and resources.
37
37
  - **A library API** with 23 subpath modules, so you can embed the browser and QA primitives in your own tooling.
38
38
  - **A plugin system** for custom viewports, sweep ops, and capture hooks.
39
39
  - **PDF reports + AI diff summaries** built in - render a sweep to a styled PDF or ask a vision LLM to describe the regression in plain language.
@@ -96,7 +96,7 @@ pursr sweep ./plan.json # see plans/ for an example
96
96
  | HAR capture | HAR 1.2 spec, written next to your shot | `--har ./req.har.json` |
97
97
  | Auth state | Playwright storageState, reuse logged-in sessions | `--auth-state admin` |
98
98
  | Plugins | custom viewports, sweep ops, before/after hooks | `pursr-plugin-*` |
99
- | MCP server | 16 tools + resources/list & resources/read for Claude/Cursor | `npx pursr-mcp` |
99
+ | MCP server | Official MCP SDK transport, 16 tools, and resources for Claude/Cursor/Codex | `npx pursr-mcp` |
100
100
  | PDF report | render sweep.json to a styled, embedded-PNG A4 PDF | `pursr report --sweep ./sweep.json` |
101
101
  | AI diff summary | vision LLM describes the diff in plain language | `pursr diff ... --ai` |
102
102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pursr",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "description": "pursr — Visual QA, audit, and MCP for the browser. One CLI + one MCP server for screenshots, sweeps, baselines, diffs, axe-core a11y audits, HAR capture, and auth state — with parallel sweep workers, auto-healing selectors, and a plugin system. Zero browser bundled: drives your system Chrome via Playwright.",
6
6
  "homepage": "https://github.com/0xheycat/pursr",
@@ -12,8 +12,8 @@
12
12
  "funding": "https://github.com/sponsors/0xheycat",
13
13
  "type": "module",
14
14
  "bin": {
15
- "pursr": "./bin/pursr.mjs",
16
- "pursr-mcp": "./bin/pursr-mcp.mjs"
15
+ "pursr": "bin/pursr.mjs",
16
+ "pursr-mcp": "bin/pursr-mcp.mjs"
17
17
  },
18
18
  "main": "./src/index.js",
19
19
  "exports": {
@@ -77,10 +77,12 @@
77
77
  ],
78
78
  "license": "MIT",
79
79
  "dependencies": {
80
+ "@modelcontextprotocol/sdk": "^1.29.0",
80
81
  "axe-core": "^4.12.1",
81
82
  "pdfkit": "^0.19.1",
82
83
  "pixelmatch": "^5.3.0",
83
- "pngjs": "^7.0.0"
84
+ "pngjs": "^7.0.0",
85
+ "zod": "^4.4.3"
84
86
  },
85
87
  "peerDependencies": {
86
88
  "playwright-core": "*"
package/src/mcp.js CHANGED
@@ -1,8 +1,7 @@
1
1
  // pursr — MCP stdio server (Model Context Protocol).
2
2
  //
3
- // Implements JSON-RPC 2.0 over stdio with Content-Length framing.
4
- // Exposes every pursr capability as an MCP tool for use by
5
- // Claude Code, Cursor, Continue, and any other MCP host.
3
+ // Uses the official Model Context Protocol SDK over stdio and exposes every
4
+ // pursr capability to Claude Code, Cursor, Codex, and other MCP hosts.
6
5
  //
7
6
  // Config via PURSR_MCP_CONFIG env or ~/./mcp-config.json:
8
7
  // { "plugins": ["./my-plugin.js"], "defaultOutDir": "./mcp-output" }
@@ -25,12 +24,20 @@ import { makeOut, nowIso } from "./util.js";
25
24
  import { listResources, readResource, recordResource } from "./mcp-resources.js";
26
25
  import { createRequire } from "node:module";
27
26
  import { BrowserSessionManager } from "./session.js";
27
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
28
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
29
+ import {
30
+ CallToolRequestSchema,
31
+ ListResourcesRequestSchema,
32
+ ListToolsRequestSchema,
33
+ ReadResourceRequestSchema,
34
+ } from "@modelcontextprotocol/sdk/types.js";
28
35
 
29
36
  const __require = createRequire(import.meta.url);
30
37
  let _pkg = { version: "0.1.0" };
31
38
  try { _pkg = __require("../package.json"); } catch {}
32
39
 
33
- const MCP_VERSION = "0.1.0";
40
+ const MCP_VERSION = _pkg.version || "0.1.0";
34
41
 
35
42
  // ─── Config ──────────────────────────────────────────────────────────────
36
43
 
@@ -60,140 +67,68 @@ class McpError extends Error {
60
67
 
61
68
  // ─── Server ──────────────────────────────────────────────────────────────
62
69
 
63
- class PursrMCPServer {
70
+ class PursrMCPServer {
64
71
  constructor(config = {}) {
65
- this.config = config;
66
- this._buffer = Buffer.alloc(0);
67
- this._contentLength = -1;
68
- this._initialized = false;
72
+ this.config = config;
69
73
  this._verbose = !!config.verbose;
70
74
  this.sessions = new BrowserSessionManager({ outputDir: config.defaultOutDir || process.cwd() });
71
- }
75
+ this.sdk = new McpServer(
76
+ { name: "pursr", version: MCP_VERSION },
77
+ {
78
+ capabilities: { tools: {}, resources: {} },
79
+ instructions: "Use a persistent pursr session for iterative visual work: open, snapshot, act, screenshot, inspect, diagnose, then close.",
80
+ },
81
+ );
82
+ this.server = this.sdk.server;
83
+ this.transport = null;
84
+ this._registerSdkHandlers();
85
+ }
72
86
 
73
87
  log(...args) {
74
88
  if (this._verbose) console.error("[pursr-mcp]", ...args);
75
89
  }
76
90
 
77
- async start() {
91
+ async start() {
78
92
  if (this.config.plugins?.length) {
79
93
  await loadPlugins(this.config.plugins);
80
94
  }
81
- this.log("server started, plugins:", listPlugins());
82
-
83
- process.stdin.on("data", (chunk) => {
84
- this._buffer = Buffer.concat([this._buffer, chunk]);
85
- this._processBuffer();
86
- });
87
- process.stdin.on("end", () => {
88
- this.log("stdin closed");
95
+ this.transport = new StdioServerTransport();
96
+ this.transport.onclose = () => {
97
+ this.log("stdio transport closed");
89
98
  this.sessions.closeAll().catch(() => {});
99
+ };
100
+ await this.sdk.connect(this.transport);
101
+ this.log("server started with official MCP SDK, plugins:", listPlugins());
102
+ }
103
+
104
+ async close() {
105
+ await this.sessions.closeAll();
106
+ await this.sdk.close();
107
+ }
108
+
109
+ _registerSdkHandlers() {
110
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: this._toolDefs() }));
111
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
112
+ try {
113
+ const content = await this._callTool(request.params.name, request.params.arguments || {});
114
+ return { content };
115
+ } catch (error) {
116
+ this.log("tool error:", error.stack || error.message);
117
+ return {
118
+ isError: true,
119
+ content: [{ type: "text", text: JSON.stringify({ error: error.message, code: error.code || -32603 }, null, 2) }],
120
+ };
121
+ }
90
122
  });
91
-
92
- process.on("uncaughtException", (err) => {
93
- console.error("[pursr-mcp] uncaught:", err.message);
94
- });
95
- }
96
-
97
- // ── Buffer framing ──────────────────────────────────────────────────
98
-
99
- _processBuffer() {
100
- while (true) {
101
- if (this._contentLength < 0) {
102
- const idx = this._buffer.indexOf(Buffer.from("\r\n\r\n"));
103
- if (idx === -1) break;
104
- const header = this._buffer.slice(0, idx).toString("utf8");
105
- const m = header.match(/Content-Length:\s*(\d+)/i);
106
- if (m) this._contentLength = parseInt(m[1], 10);
107
- this._buffer = this._buffer.slice(idx + 4);
108
- }
109
- if (this._contentLength > 0 && this._buffer.length >= this._contentLength) {
110
- const raw = this._buffer.slice(0, this._contentLength).toString("utf8");
111
- this._buffer = this._buffer.slice(this._contentLength);
112
- this._contentLength = -1;
113
- try {
114
- const msg = JSON.parse(raw);
115
- this._handleMessage(msg);
116
- } catch (e) {
117
- console.error("[pursr-mcp] invalid JSON:", e.message);
118
- }
119
- } else break;
120
- }
121
- }
122
-
123
- _send(msg) {
124
- const json = JSON.stringify(msg);
125
- const bytes = Buffer.from(json, "utf8");
126
- const header = `Content-Length: ${bytes.length}\r\n\r\n`;
127
- process.stdout.write(header);
128
- process.stdout.write(bytes);
129
- }
130
-
131
- // ── JSON-RPC dispatcher ─────────────────────────────────────────────
132
-
133
- async _handleMessage(msg) {
134
- if (!msg || msg.jsonrpc !== "2.0" || !msg.method) {
135
- console.error("[pursr-mcp] skipping non-JSON-RPC message");
136
- return;
137
- }
138
- const { method, id } = msg;
139
-
140
- // Notifications — no id → no response
141
- if (method === "notifications/initialized" || method === "notifications/cancelled") {
142
- if (method === "notifications/initialized") this._initialized = true;
143
- return;
144
- }
145
- if (id === undefined || id === null) return; // unnamed notification
146
-
147
- try {
148
- switch (method) {
149
- case "initialize":
150
- this._initialized = true;
151
- this._send({
152
- jsonrpc: "2.0", id,
153
- result: {
154
- protocolVersion: msg.params?.protocolVersion || "2024-11-05",
155
- capabilities: { tools: {} },
156
- serverInfo: { name: "pursr", version: MCP_VERSION },
157
- },
158
- });
159
- break;
160
-
161
- case "tools/list":
162
- this._send({ jsonrpc: "2.0", id, result: { tools: this._toolDefs() } });
163
- break;
164
-
165
- case "resources/list":
166
- this._send({ jsonrpc: "2.0", id, result: { resources: listResources().map(this._toMcpResource, this) } });
167
- break;
168
-
169
- case "resources/read":
170
- if (!msg.params?.uri) throw new McpError(-32602, "Missing uri");
171
- const data = readResource(msg.params.uri);
172
- if (!data) throw new McpError(-32602, "Resource not found: " + msg.params.uri);
173
- this._send({ jsonrpc: "2.0", id, result: { contents: [data] } });
174
- break;
175
-
176
- case "tools/call":
177
- if (!msg.params?.name) throw new McpError(-32602, "Missing tool name");
178
- const result = await this._callTool(msg.params.name, msg.params.arguments || {});
179
- this._send({ jsonrpc: "2.0", id, result: { content: result } });
180
- break;
181
-
182
- default:
183
- this._send({
184
- jsonrpc: "2.0", id,
185
- error: { code: -32601, message: `Unknown method: ${method}` },
186
- });
187
- }
188
- } catch (e) {
189
- if (e instanceof McpError) {
190
- this._send({ jsonrpc: "2.0", id, error: { code: e.code, message: e.message } });
191
- } else {
192
- console.error("[pursr-mcp] handler error:", e.stack || e.message);
193
- this._send({ jsonrpc: "2.0", id, error: { code: -32603, message: e.message } });
194
- }
195
- }
196
- }
123
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
124
+ resources: listResources().map(this._toMcpResource, this),
125
+ }));
126
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
127
+ const data = readResource(request.params.uri);
128
+ if (!data) throw new Error("Resource not found: " + request.params.uri);
129
+ return { contents: [data] };
130
+ });
131
+ }
197
132
 
198
133
  // ── Resource shape adapter ─────────────────────────────────────────
199
134