@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 +140 -0
- package/package.json +13 -0
- package/src/api.mjs +54 -0
- package/src/index.mjs +100 -0
- package/src/tools.mjs +308 -0
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
|
+
}
|