@glance-mcp/server 1.0.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/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # @glance-mcp/server
2
+
3
+ MCP server for [Glance](https://glance.tools) — capture what a web page looks like and does. Screenshots at three viewports, network traffic, console output, rendered DOM, visible text, and structured metadata.
4
+
5
+ Zero dependencies. Node 18+ builtins only. No build step.
6
+
7
+ ## Setup
8
+
9
+ ### Claude Desktop
10
+
11
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "glance": {
17
+ "command": "node",
18
+ "args": ["/path/to/node_modules/@glance-mcp/server/src/index.mjs"],
19
+ "env": { "GLANCE_API_KEY": "gl_live_..." }
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ ### Cursor
26
+
27
+ Add to Cursor's MCP settings (Settings > MCP Servers):
28
+
29
+ ```json
30
+ {
31
+ "glance": {
32
+ "command": "node",
33
+ "args": ["/path/to/node_modules/@glance-mcp/server/src/index.mjs"],
34
+ "env": { "GLANCE_API_KEY": "gl_live_..." }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### Claude Code
40
+
41
+ ```bash
42
+ claude mcp add glance node /path/to/node_modules/@glance-mcp/server/src/index.mjs
43
+ ```
44
+
45
+ Set `GLANCE_API_KEY` in your environment.
46
+
47
+ ### npx (any MCP host)
48
+
49
+ ```json
50
+ {
51
+ "command": "npx",
52
+ "args": ["-y", "@glance-mcp/server"],
53
+ "env": { "GLANCE_API_KEY": "gl_live_..." }
54
+ }
55
+ ```
56
+
57
+ ## Getting an API key
58
+
59
+ No key? The `signup` tool provisions one from inside the MCP session — no dashboard, no credit card. 100 free captures/month.
60
+
61
+ Or, from a terminal:
62
+
63
+ ```bash
64
+ curl -s https://api.glance.tools/v1 -d '{
65
+ "jsonrpc": "2.0", "method": "signup",
66
+ "params": {"email": "you@example.com"}, "id": 1
67
+ }'
68
+ ```
69
+
70
+ ## Tools
71
+
72
+ ### `glance`
73
+
74
+ Capture a web page. Submits the job, polls for completion, and returns artifact URLs in a single call.
75
+
76
+ | Param | Type | Description |
77
+ |---|---|---|
78
+ | `url` | string | Absolute URL to capture. Required. |
79
+ | `waitMs` | integer | Max ms to wait for network idle. Default: 2000. |
80
+ | `settleMs` | integer | Extra ms to wait after idle. Default: 0. |
81
+ | `dom` | boolean | Include rendered DOM. Default: true. |
82
+ | `text` | boolean | Include visible text. Default: true. |
83
+ | `meta` | boolean | Extract JSON-LD, Open Graph, title. Default: true. |
84
+ | `only` | string[] | Whitelist: `screenshots`, `network`, `console`, `dom`, `text`, `meta`. |
85
+ | `burst` | integer | Capture N frames for transition analysis. |
86
+ | `click` | string | CSS selector to click at burst midpoint. |
87
+ | `interactions` | object[] | Scripted interaction sequence (see below). |
88
+
89
+ ### `usage`
90
+
91
+ Check remaining API quota for the current billing period.
92
+
93
+ ### `feedback`
94
+
95
+ Submit freeform feedback about Glance.
96
+
97
+ ### `hello`
98
+
99
+ Service info, capabilities, and free tier details. Works without an API key.
100
+
101
+ ### `signup`
102
+
103
+ Provision an API key from an email address. The key is stored in memory for the rest of the session.
104
+
105
+ ## Interactions
106
+
107
+ The `interactions` param on `glance` accepts an array of sequential actions:
108
+
109
+ ```json
110
+ [
111
+ {"type": "type", "selector": "#search", "value": "query"},
112
+ {"type": "click", "selector": "#submit"},
113
+ {"type": "wait", "ms": 2000},
114
+ {"type": "screenshot", "name": "results"}
115
+ ]
116
+ ```
117
+
118
+ | Action | Fields | Description |
119
+ |---|---|---|
120
+ | `wait` | `ms` | Pause (default 1000ms). |
121
+ | `click` | `selector` | Click an element. |
122
+ | `type` | `selector`, `value` | Set an input's value. |
123
+ | `keypress` | `key` | Dispatch a key event. |
124
+ | `eval` | `script` | Run JavaScript in the page. |
125
+ | `screenshot` | `name` | Capture the viewport at this point. |
126
+
127
+ ## Environment variables
128
+
129
+ | Variable | Description |
130
+ |---|---|
131
+ | `GLANCE_API_KEY` | API key for authenticated methods. |
132
+ | `GLANCE_API_URL` | Override the API endpoint (default: `https://api.glance.tools/v1`). |
133
+
134
+ ## How it works
135
+
136
+ The server speaks [MCP](https://modelcontextprotocol.io) (Model Context Protocol) over stdio — JSON-RPC 2.0 with newline framing. It's a client of the [Glance hosted API](https://glance.tools), not a replacement for it. No engine, no Chrome, no local browser needed.
137
+
138
+ ## License
139
+
140
+ MIT
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@glance-mcp/server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Glance — capture what a web page looks like and does",
5
+ "type": "module",
6
+ "bin": { "glance-mcp": "./src/index.mjs" },
7
+ "main": "./src/index.mjs",
8
+ "files": ["src"],
9
+ "engines": { "node": ">=18" },
10
+ "license": "MIT",
11
+ "homepage": "https://glance.tools",
12
+ "keywords": ["mcp", "mcp-server", "glance", "screenshot", "web-capture", "model-context-protocol", "claude", "cursor", "web-scraping", "headless-chrome", "llm-tools"]
13
+ }
package/src/api.mjs ADDED
@@ -0,0 +1,54 @@
1
+ // JSON-RPC HTTP client for the Glance /v1 endpoint.
2
+ // Zero dependencies — uses Node 18+ global fetch.
3
+
4
+ const DEFAULT_API_URL = 'https://api.glance.tools/v1';
5
+
6
+ export function createApiClient({ apiKey, apiUrl } = {}) {
7
+ let key = apiKey || null;
8
+ const url = apiUrl || process.env.GLANCE_API_URL || DEFAULT_API_URL;
9
+
10
+ async function rpc(method, params) {
11
+ const body = JSON.stringify({
12
+ jsonrpc: '2.0',
13
+ method,
14
+ params: params || {},
15
+ id: 1,
16
+ });
17
+
18
+ const headers = { 'Content-Type': 'application/json' };
19
+ if (key) headers['Authorization'] = `Bearer ${key}`;
20
+
21
+ const res = await fetch(url, { method: 'POST', headers, body });
22
+
23
+ if (!res.ok) {
24
+ const text = await res.text().catch(() => '');
25
+ const err = new Error(`HTTP ${res.status}: ${text || res.statusText}`);
26
+ err.code = -32000;
27
+ throw err;
28
+ }
29
+
30
+ const json = await res.json();
31
+
32
+ if (json.error) {
33
+ const err = new Error(json.error.message);
34
+ err.code = json.error.code;
35
+ err.data = json.error.data || null;
36
+ throw err;
37
+ }
38
+
39
+ return json.result;
40
+ }
41
+
42
+ return {
43
+ get apiKey() { return key; },
44
+
45
+ setApiKey(newKey) { key = newKey; },
46
+
47
+ hello() { return rpc('hello'); },
48
+ signup(email) { return rpc('signup', { email }); },
49
+ glance(params) { return rpc('glance', params); },
50
+ result(job) { return rpc('result', { job }); },
51
+ usage() { return rpc('usage'); },
52
+ feedback(message){ return rpc('feedback', { message }); },
53
+ };
54
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+
3
+ // MCP server for Glance — stdio transport, JSON-RPC 2.0.
4
+ // Zero dependencies. Node 18+ builtins only.
5
+
6
+ import { createInterface } from 'readline';
7
+ import { createApiClient } from './api.mjs';
8
+ import { tools, handleToolCall } from './tools.mjs';
9
+
10
+ const SERVER_NAME = 'glance-mcp';
11
+ const SERVER_VERSION = '1.0.0';
12
+ const PROTOCOL_VERSION = '2025-06-18';
13
+
14
+ // ── API client ──────────────────────────────────────────────────
15
+
16
+ const api = createApiClient({
17
+ apiKey: process.env.GLANCE_API_KEY || null,
18
+ apiUrl: process.env.GLANCE_API_URL || null,
19
+ });
20
+
21
+ // ── JSON-RPC helpers ────────────────────────────────────────────
22
+
23
+ function respond(id, result) {
24
+ const msg = JSON.stringify({ jsonrpc: '2.0', result, id });
25
+ process.stdout.write(msg + '\n');
26
+ }
27
+
28
+ function respondError(id, code, message, data) {
29
+ const err = { code, message };
30
+ if (data !== undefined) err.data = data;
31
+ const msg = JSON.stringify({ jsonrpc: '2.0', error: err, id });
32
+ process.stdout.write(msg + '\n');
33
+ }
34
+
35
+ // ── Method dispatch ─────────────────────────────────────────────
36
+
37
+ async function handleMessage(line) {
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(line);
41
+ } catch {
42
+ respondError(null, -32700, 'Parse error');
43
+ return;
44
+ }
45
+
46
+ const { id, method, params } = parsed;
47
+
48
+ // Notifications (no id) — acknowledge silently
49
+ if (id === undefined || id === null) {
50
+ // notifications/initialized, etc. — no response expected
51
+ return;
52
+ }
53
+
54
+ switch (method) {
55
+ case 'initialize':
56
+ respond(id, {
57
+ protocolVersion: PROTOCOL_VERSION,
58
+ capabilities: { tools: {} },
59
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
60
+ });
61
+ break;
62
+
63
+ case 'ping':
64
+ respond(id, {});
65
+ break;
66
+
67
+ case 'tools/list':
68
+ respond(id, { tools });
69
+ break;
70
+
71
+ case 'tools/call': {
72
+ const toolName = params?.name;
73
+ const toolArgs = params?.arguments || {};
74
+ const result = await handleToolCall(toolName, toolArgs, api);
75
+ respond(id, result);
76
+ break;
77
+ }
78
+
79
+ default:
80
+ respondError(id, -32601, 'Method not found', { method });
81
+ }
82
+ }
83
+
84
+ // ── stdio transport ─────────────────────────────────────────────
85
+
86
+ const rl = createInterface({ input: process.stdin });
87
+
88
+ rl.on('line', (line) => {
89
+ const trimmed = line.trim();
90
+ if (!trimmed) return;
91
+ handleMessage(trimmed).catch((err) => {
92
+ process.stderr.write(`[glance-mcp] Unhandled error: ${err.message}\n`);
93
+ });
94
+ });
95
+
96
+ rl.on('close', () => {
97
+ process.exit(0);
98
+ });
99
+
100
+ process.stderr.write(`[glance-mcp] Server started. Waiting for MCP messages on stdin.\n`);
package/src/tools.mjs ADDED
@@ -0,0 +1,308 @@
1
+ // MCP tool definitions (JSON Schema) and handlers.
2
+ // Each tool wraps one or more Glance /v1 JSON-RPC methods.
3
+
4
+ const POLL_INTERVAL_MS = 1000;
5
+ const POLL_TIMEOUT_MS = 20_000;
6
+
7
+ // ── Tool definitions ────────────────────────────────────────────
8
+
9
+ export const tools = [
10
+ {
11
+ name: 'glance',
12
+ description:
13
+ 'Capture what a web page looks like and does. Takes a screenshot of the page at desktop, ' +
14
+ 'tablet, and mobile viewports and returns URLs to the images. Also captures network traffic, ' +
15
+ 'console output, rendered DOM, visible text, and structured metadata. Supports scripted ' +
16
+ 'interactions (click, type, wait, keypress, eval, screenshot) to capture pages after user actions.',
17
+ inputSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ url: {
21
+ type: 'string',
22
+ description: 'Absolute URL to capture.',
23
+ },
24
+ waitMs: {
25
+ type: 'integer',
26
+ description: 'Max ms to wait for network idle after load. Default: 2000.',
27
+ },
28
+ settleMs: {
29
+ type: 'integer',
30
+ description: 'Extra ms to wait after network idle before capturing. Default: 0.',
31
+ },
32
+ dom: {
33
+ type: 'boolean',
34
+ description: 'Include rendered DOM (document.documentElement.outerHTML). Default: true.',
35
+ },
36
+ text: {
37
+ type: 'boolean',
38
+ description: 'Include visible text (document.body.innerText). Default: true.',
39
+ },
40
+ meta: {
41
+ type: 'boolean',
42
+ description: 'Extract JSON-LD, Open Graph, description, title. Default: true.',
43
+ },
44
+ only: {
45
+ type: 'array',
46
+ items: { type: 'string' },
47
+ description:
48
+ 'Whitelist of artifact types to produce. Options: screenshots, network, console, dom, text, meta. ' +
49
+ 'When omitted, all enabled artifacts are produced.',
50
+ },
51
+ burst: {
52
+ type: 'integer',
53
+ description: 'Capture N screenshot frames for transition/FOUC analysis.',
54
+ },
55
+ click: {
56
+ type: 'string',
57
+ description: 'CSS selector to click at midpoint of a burst sequence.',
58
+ },
59
+ interactions: {
60
+ type: 'array',
61
+ description:
62
+ 'Scripted interaction sequence. Each action has a "type" and type-specific fields. ' +
63
+ 'Actions execute sequentially after network idle.',
64
+ items: {
65
+ type: 'object',
66
+ properties: {
67
+ type: {
68
+ type: 'string',
69
+ enum: ['wait', 'click', 'type', 'keypress', 'eval', 'screenshot'],
70
+ description: 'Action type.',
71
+ },
72
+ ms: {
73
+ type: 'integer',
74
+ description: 'For "wait": milliseconds to pause. Default: 1000.',
75
+ },
76
+ selector: {
77
+ type: 'string',
78
+ description: 'For "click" and "type": CSS selector for the target element.',
79
+ },
80
+ value: {
81
+ type: 'string',
82
+ description: 'For "type": text to enter into the input.',
83
+ },
84
+ key: {
85
+ type: 'string',
86
+ description:
87
+ 'For "keypress": key name. Supported: ArrowRight, ArrowLeft, ArrowUp, ArrowDown, ' +
88
+ 'Enter, Escape, Tab, Space, Backspace, Delete, Home, End, PageUp, PageDown.',
89
+ },
90
+ script: {
91
+ type: 'string',
92
+ description: 'For "eval": JavaScript expression to evaluate in the page context.',
93
+ },
94
+ name: {
95
+ type: 'string',
96
+ description: 'For "screenshot": filename (without extension). Default: interaction-{n}.',
97
+ },
98
+ },
99
+ required: ['type'],
100
+ },
101
+ },
102
+ },
103
+ required: ['url'],
104
+ },
105
+ },
106
+ {
107
+ name: 'usage',
108
+ description: 'Check remaining Glance API quota for the current billing period.',
109
+ inputSchema: {
110
+ type: 'object',
111
+ properties: {},
112
+ },
113
+ },
114
+ {
115
+ name: 'feedback',
116
+ description: 'Submit freeform feedback about Glance.',
117
+ inputSchema: {
118
+ type: 'object',
119
+ properties: {
120
+ message: {
121
+ type: 'string',
122
+ description: 'Feedback message. Max 2000 characters.',
123
+ },
124
+ },
125
+ required: ['message'],
126
+ },
127
+ },
128
+ {
129
+ name: 'hello',
130
+ description: 'Get Glance service info — capabilities, methods, and free tier details. No API key required.',
131
+ inputSchema: {
132
+ type: 'object',
133
+ properties: {},
134
+ },
135
+ },
136
+ {
137
+ name: 'signup',
138
+ description:
139
+ 'Create a Glance account and provision an API key. Free tier: 100 captures/month. ' +
140
+ 'No credit card required. The key is stored for the session — no need to set it manually.',
141
+ inputSchema: {
142
+ type: 'object',
143
+ properties: {
144
+ email: {
145
+ type: 'string',
146
+ description: 'Email address for the account. Same email always returns the same key.',
147
+ },
148
+ },
149
+ required: ['email'],
150
+ },
151
+ },
152
+ ];
153
+
154
+ // ── Tool handlers ───────────────────────────────────────────────
155
+
156
+ function errorResult(text) {
157
+ return { content: [{ type: 'text', text }], isError: true };
158
+ }
159
+
160
+ function textResult(text) {
161
+ return { content: [{ type: 'text', text }] };
162
+ }
163
+
164
+ function formatApiError(err) {
165
+ const code = err.code;
166
+ const data = err.data;
167
+
168
+ if (code === -32000) {
169
+ return errorResult(
170
+ 'Authentication required. Set the GLANCE_API_KEY environment variable, or use the ' +
171
+ 'signup tool to create an account and get a key.'
172
+ );
173
+ }
174
+ if (code === -32003) {
175
+ const parts = ['Quota exhausted.'];
176
+ if (data?.used != null) parts.push(`Used: ${data.used}/${data.limit}.`);
177
+ if (data?.resets_at) parts.push(`Resets at: ${data.resets_at}.`);
178
+ return errorResult(parts.join(' '));
179
+ }
180
+ if (code === -32004) {
181
+ return errorResult(
182
+ 'Concurrency limit reached — too many captures running. Wait for running jobs to finish and try again.'
183
+ );
184
+ }
185
+
186
+ return errorResult(err.message || 'Unknown error');
187
+ }
188
+
189
+ function sleep(ms) {
190
+ return new Promise(resolve => setTimeout(resolve, ms));
191
+ }
192
+
193
+ async function handleGlance(args, api) {
194
+ // Build params — serialize interactions to JSON string for the API
195
+ const params = { ...args };
196
+ if (Array.isArray(params.interactions)) {
197
+ params.interactions = JSON.stringify(params.interactions);
198
+ }
199
+
200
+ const accepted = await api.glance(params);
201
+ const jobId = accepted.job;
202
+
203
+ // Poll for result
204
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
205
+ while (Date.now() < deadline) {
206
+ await sleep(POLL_INTERVAL_MS);
207
+ const res = await api.result(jobId);
208
+
209
+ if (res.status === 'done') {
210
+ const lines = [`Capture complete for ${args.url}`, ''];
211
+
212
+ if (res.artifacts) {
213
+ const { meta, ...urlArtifacts } = res.artifacts;
214
+
215
+ // Screenshot and file URLs
216
+ for (const [key, value] of Object.entries(urlArtifacts)) {
217
+ if (typeof value === 'string') {
218
+ lines.push(`${key}: ${value}`);
219
+ }
220
+ }
221
+
222
+ // Inline meta
223
+ if (meta && typeof meta === 'object') {
224
+ lines.push('');
225
+ lines.push('meta: ' + JSON.stringify(meta, null, 2));
226
+ }
227
+ }
228
+
229
+ if (res.usage) {
230
+ lines.push('');
231
+ lines.push(`Usage: ${res.usage.used} used, ${res.usage.remaining} remaining`);
232
+ }
233
+
234
+ return textResult(lines.join('\n'));
235
+ }
236
+
237
+ if (res.status === 'failed') {
238
+ return errorResult(
239
+ `Capture failed for ${args.url}: ${res.error || 'unknown'}` +
240
+ (res.detail ? ` — ${res.detail}` : '')
241
+ );
242
+ }
243
+ }
244
+
245
+ return errorResult(`Capture timed out after ${POLL_TIMEOUT_MS / 1000}s for ${args.url} (job: ${jobId}).`);
246
+ }
247
+
248
+ async function handleUsage(args, api) {
249
+ const res = await api.usage();
250
+ const lines = [
251
+ `Tier: ${res.tier}`,
252
+ `Used: ${res.used} / ${res.calls_per_month}`,
253
+ `Running: ${res.running}`,
254
+ `Resets at: ${res.resets_at}`,
255
+ ];
256
+ return textResult(lines.join('\n'));
257
+ }
258
+
259
+ async function handleFeedback(args, api) {
260
+ await api.feedback(args.message);
261
+ return textResult('Feedback received. Thank you.');
262
+ }
263
+
264
+ async function handleHello(args, api) {
265
+ const res = await api.hello();
266
+ const lines = [
267
+ res.glance,
268
+ '',
269
+ `Version: ${res.version}`,
270
+ `Capabilities: ${res.capabilities.join(', ')}`,
271
+ `Free tier: ${res.free_tier.calls_per_month} calls/month`,
272
+ `Methods: ${res.methods.join(', ')}`,
273
+ ];
274
+ return textResult(lines.join('\n'));
275
+ }
276
+
277
+ async function handleSignup(args, api) {
278
+ const res = await api.signup(args.email);
279
+ api.setApiKey(res.api_key);
280
+ return textResult(
281
+ `Account ready. Tier: ${res.tier}, ${res.calls_per_month} calls/month. ` +
282
+ 'API key is active for this session.'
283
+ );
284
+ }
285
+
286
+ const handlers = {
287
+ glance: handleGlance,
288
+ usage: handleUsage,
289
+ feedback: handleFeedback,
290
+ hello: handleHello,
291
+ signup: handleSignup,
292
+ };
293
+
294
+ export async function handleToolCall(name, args, api) {
295
+ const handler = handlers[name];
296
+ if (!handler) {
297
+ return errorResult(`Unknown tool: ${name}`);
298
+ }
299
+ try {
300
+ return await handler(args || {}, api);
301
+ } catch (err) {
302
+ if (err.code && err.code <= -32000) {
303
+ return formatApiError(err);
304
+ }
305
+ // Network / unexpected errors
306
+ return errorResult(`API request failed: ${err.message}`);
307
+ }
308
+ }