@glance-mcp/server 1.1.0 → 1.2.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 CHANGED
@@ -92,15 +92,16 @@ Check remaining API quota for the current billing period.
92
92
 
93
93
  ### `feedback`
94
94
 
95
- Submit freeform feedback about Glance.
95
+ Send feedback to the Glance team. Supports threading: include a `thread_id` to continue an existing conversation, or omit it to start a new one. Returns a `thread_id` for follow-ups and any operator replies.
96
96
 
97
- ### `hello`
98
-
99
- Service info, capabilities, and free tier details. Works without an API key.
97
+ | Param | Type | Description |
98
+ |---|---|---|
99
+ | `message` | string | Feedback message. Max 2000 characters. Required. |
100
+ | `thread_id` | string | Thread ID from a previous feedback call. Omit to start a new thread. |
100
101
 
101
102
  ### `signup`
102
103
 
103
- Provision an API key from an email address. The key is stored in memory for the rest of the session.
104
+ Provision an API key from an email address. The key is active for the current session; the response includes ready-to-run commands to persist it in your MCP config.
104
105
 
105
106
  ## Interactions
106
107
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glance-mcp/server",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for Glance — capture what a web page looks like and does",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.mjs CHANGED
@@ -52,15 +52,15 @@ export function createApiClient({ apiKey, apiUrl } = {}) {
52
52
 
53
53
  return {
54
54
  get apiKey() { return key; },
55
-
56
55
  setApiKey(newKey) { key = newKey; },
56
+ rpc,
57
+ fetchImage,
57
58
 
58
- hello() { return rpc('hello'); },
59
59
  signup(email) { return rpc('signup', { email }); },
60
60
  glance(params) { return rpc('glance', params); },
61
61
  result(job) { return rpc('result', { job }); },
62
62
  usage() { return rpc('usage'); },
63
- feedback(message){ return rpc('feedback', { message }); },
64
- fetchImage,
63
+ feedbackSend(message, threadId) { return rpc('feedback.send', { message, thread_id: threadId }); },
64
+ feedbackGet(threadId) { return rpc('feedback.get', { thread_id: threadId }); },
65
65
  };
66
66
  }
package/src/index.mjs CHANGED
@@ -5,10 +5,42 @@
5
5
 
6
6
  import { createInterface } from 'readline';
7
7
  import { createApiClient } from './api.mjs';
8
- import { tools, handleToolCall } from './tools.mjs';
8
+ import { formatApiError, errorResult } from './tools/_helpers.mjs';
9
+ import * as meta from './tools/_meta.mjs';
10
+ import * as glance from './tools/glance.mjs';
11
+
12
+ // ── Tool list (served by tools/list) ────────────────────────────
13
+
14
+ const tools = [...meta.definitions, glance.definition];
15
+
16
+ // ── Tool dispatch ───────────────────────────────────────────────
17
+
18
+ const metaNames = new Set(meta.definitions.map(d => d.name));
19
+
20
+ async function handleToolCall(name, args, api) {
21
+ try {
22
+ if (metaNames.has(name)) {
23
+ return await meta.handler(name, args || {}, api);
24
+ }
25
+ if (name === 'glance') {
26
+ return await glance.handler(args || {}, api);
27
+ }
28
+ return errorResult(`Unknown tool: ${name}`);
29
+ } catch (err) {
30
+ if (err.code && err.code <= -32000) {
31
+ return formatApiError(err);
32
+ }
33
+ return errorResult(`API request failed: ${err.message}`);
34
+ }
35
+ }
9
36
 
10
37
  const SERVER_NAME = 'glance-mcp';
11
38
  const SERVER_VERSION = '1.0.0';
39
+ const SERVER_DESCRIPTION =
40
+ 'Glance — web capture for AI agents. ' +
41
+ 'Give Glance a URL and get back screenshots (desktop, tablet, mobile), ' +
42
+ 'network traffic, console output, rendered DOM, visible text, and structured metadata. ' +
43
+ 'To get started, call signup with an email (free, no credit card), then call glance with a URL.';
12
44
  const PROTOCOL_VERSION = '2025-06-18';
13
45
 
14
46
  // ── API client ──────────────────────────────────────────────────
@@ -56,7 +88,7 @@ async function handleMessage(line) {
56
88
  respond(id, {
57
89
  protocolVersion: PROTOCOL_VERSION,
58
90
  capabilities: { tools: {} },
59
- serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
91
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION, description: SERVER_DESCRIPTION },
60
92
  });
61
93
  break;
62
94
 
@@ -0,0 +1,43 @@
1
+ // Shared helpers for tool handlers.
2
+
3
+ const POLL_INTERVAL_MS = 1000;
4
+ const POLL_TIMEOUT_MS = 20_000;
5
+
6
+ export { POLL_INTERVAL_MS, POLL_TIMEOUT_MS };
7
+
8
+ export function errorResult(text) {
9
+ return { content: [{ type: 'text', text }], isError: true };
10
+ }
11
+
12
+ export function textResult(text) {
13
+ return { content: [{ type: 'text', text }] };
14
+ }
15
+
16
+ export function formatApiError(err) {
17
+ const code = err.code;
18
+ const data = err.data;
19
+
20
+ if (code === -32000) {
21
+ return errorResult(
22
+ 'Authentication required. Set the GLANCE_API_KEY environment variable, or use the ' +
23
+ 'signup tool to create an account and get a key.'
24
+ );
25
+ }
26
+ if (code === -32003) {
27
+ const parts = ['Quota exhausted.'];
28
+ if (data?.used != null) parts.push(`Used: ${data.used}/${data.limit}.`);
29
+ if (data?.resets_at) parts.push(`Resets at: ${data.resets_at}.`);
30
+ return errorResult(parts.join(' '));
31
+ }
32
+ if (code === -32004) {
33
+ return errorResult(
34
+ 'Concurrency limit reached — too many captures running. Wait for running jobs to finish and try again.'
35
+ );
36
+ }
37
+
38
+ return errorResult(err.message || 'Unknown error');
39
+ }
40
+
41
+ export function sleep(ms) {
42
+ return new Promise(resolve => setTimeout(resolve, ms));
43
+ }
@@ -0,0 +1,101 @@
1
+ // Platform tools — usage, feedback, signup.
2
+ // Always included regardless of GLANCE_TOOLS filtering.
3
+
4
+ import { textResult } from './_helpers.mjs';
5
+
6
+ // ── Definitions ─────────────────────────────────────────────────
7
+
8
+ export const definitions = [
9
+ {
10
+ name: 'usage',
11
+ description: 'Check remaining Glance API quota for the current billing period.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {},
15
+ },
16
+ },
17
+ {
18
+ name: 'feedback',
19
+ description:
20
+ 'Send feedback to the Glance team. Supports threading: include a thread_id to continue ' +
21
+ 'an existing conversation, or omit it to start a new one. Returns a thread_id for follow-ups ' +
22
+ 'and any operator replies.',
23
+ inputSchema: {
24
+ type: 'object',
25
+ properties: {
26
+ message: {
27
+ type: 'string',
28
+ description: 'Feedback message. Max 2000 characters.',
29
+ },
30
+ thread_id: {
31
+ type: 'string',
32
+ description: 'Thread ID from a previous feedback call. Omit to start a new thread.',
33
+ },
34
+ },
35
+ required: ['message'],
36
+ },
37
+ },
38
+ {
39
+ name: 'signup',
40
+ description:
41
+ 'Create a Glance account and provision an API key. Free tier: 100 captures/month, no credit card. ' +
42
+ 'IMPORTANT: The key is active for this session only. You MUST present the persistence command ' +
43
+ 'from the response to the user so they can save the key for future sessions.',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: {
47
+ email: {
48
+ type: 'string',
49
+ description: 'Email address for the account. Same email always returns the same key.',
50
+ },
51
+ },
52
+ required: ['email'],
53
+ },
54
+ },
55
+ ];
56
+
57
+ // ── Handlers ────────────────────────────────────────────────────
58
+
59
+ const handlers = {
60
+ async usage(args, api) {
61
+ const res = await api.usage();
62
+ const lines = [
63
+ `Tier: ${res.tier}`,
64
+ `Used: ${res.used} / ${res.calls_per_month}`,
65
+ `Running: ${res.running}`,
66
+ `Resets at: ${res.resets_at}`,
67
+ ];
68
+ return textResult(lines.join('\n'));
69
+ },
70
+
71
+ async feedback(args, api) {
72
+ const res = await api.feedbackSend(args.message, args.thread_id);
73
+ const lines = [`Feedback received. Thread: ${res.thread_id}`];
74
+ if (res.replies.length > 0) {
75
+ lines.push('', `Replies (${res.replies.length}):`);
76
+ for (const r of res.replies) {
77
+ lines.push(` [${r.created_at}] ${r.message}`);
78
+ }
79
+ }
80
+ return textResult(lines.join('\n'));
81
+ },
82
+
83
+ async signup(args, api) {
84
+ const res = await api.signup(args.email);
85
+ api.setApiKey(res.api_key);
86
+ const key = res.api_key;
87
+ return textResult(
88
+ `Account ready. API key: ${key}\n` +
89
+ `Tier: ${res.tier}, ${res.calls_per_month} calls/month.\n\n` +
90
+ '⚠️ PERSIST YOUR KEY — it only lives in memory for this session.\n' +
91
+ 'Present one of these commands to the user so they can save it:\n\n' +
92
+ ` Claude Code: claude mcp update-env glance GLANCE_API_KEY=${key}\n` +
93
+ ` Manual: Add to the "env" block in your MCP server config:\n` +
94
+ ` "env": { "GLANCE_API_KEY": "${key}" }`
95
+ );
96
+ },
97
+ };
98
+
99
+ export function handler(name, args, api) {
100
+ return handlers[name](args, api);
101
+ }
@@ -0,0 +1,179 @@
1
+ // Glance — capture what a web page looks like and does.
2
+
3
+ import { errorResult, sleep, POLL_INTERVAL_MS, POLL_TIMEOUT_MS } from './_helpers.mjs';
4
+
5
+ export const definition = {
6
+ name: 'glance',
7
+ description:
8
+ 'Capture what a web page looks like and does. Takes a screenshot of the page at desktop, ' +
9
+ 'tablet, and mobile viewports and returns URLs to the images. Also captures network traffic, ' +
10
+ 'console output, rendered DOM, visible text, and structured metadata. Supports scripted ' +
11
+ 'interactions (click, type, wait, keypress, eval, screenshot) to capture pages after user actions.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ url: {
16
+ type: 'string',
17
+ description: 'Absolute URL to capture.',
18
+ },
19
+ waitMs: {
20
+ type: 'integer',
21
+ description: 'Max ms to wait for network idle after load. Default: 2000.',
22
+ },
23
+ settleMs: {
24
+ type: 'integer',
25
+ description: 'Extra ms to wait after network idle before capturing. Default: 0.',
26
+ },
27
+ dom: {
28
+ type: 'boolean',
29
+ description: 'Include rendered DOM (document.documentElement.outerHTML). Default: true.',
30
+ },
31
+ text: {
32
+ type: 'boolean',
33
+ description: 'Include visible text (document.body.innerText). Default: true.',
34
+ },
35
+ meta: {
36
+ type: 'boolean',
37
+ description: 'Extract JSON-LD, Open Graph, description, title. Default: true.',
38
+ },
39
+ only: {
40
+ type: 'array',
41
+ items: { type: 'string' },
42
+ description:
43
+ 'Whitelist of artifact types to produce. Options: screenshots, network, console, dom, text, meta. ' +
44
+ 'When omitted, all enabled artifacts are produced.',
45
+ },
46
+ burst: {
47
+ type: 'integer',
48
+ description: 'Capture N screenshot frames for transition/FOUC analysis.',
49
+ },
50
+ click: {
51
+ type: 'string',
52
+ description: 'CSS selector to click at midpoint of a burst sequence.',
53
+ },
54
+ interactions: {
55
+ type: 'array',
56
+ description:
57
+ 'Scripted interaction sequence. Each action has a "type" and type-specific fields. ' +
58
+ 'Actions execute sequentially after network idle.',
59
+ items: {
60
+ type: 'object',
61
+ properties: {
62
+ type: {
63
+ type: 'string',
64
+ enum: ['wait', 'click', 'type', 'keypress', 'eval', 'screenshot'],
65
+ description: 'Action type.',
66
+ },
67
+ ms: {
68
+ type: 'integer',
69
+ description: 'For "wait": milliseconds to pause. Default: 1000.',
70
+ },
71
+ selector: {
72
+ type: 'string',
73
+ description: 'For "click" and "type": CSS selector for the target element.',
74
+ },
75
+ value: {
76
+ type: 'string',
77
+ description: 'For "type": text to enter into the input.',
78
+ },
79
+ key: {
80
+ type: 'string',
81
+ description:
82
+ 'For "keypress": key name. Supported: ArrowRight, ArrowLeft, ArrowUp, ArrowDown, ' +
83
+ 'Enter, Escape, Tab, Space, Backspace, Delete, Home, End, PageUp, PageDown.',
84
+ },
85
+ script: {
86
+ type: 'string',
87
+ description: 'For "eval": JavaScript expression to evaluate in the page context.',
88
+ },
89
+ name: {
90
+ type: 'string',
91
+ description: 'For "screenshot": filename (without extension). Default: interaction-{n}.',
92
+ },
93
+ },
94
+ required: ['type'],
95
+ },
96
+ },
97
+ },
98
+ required: ['url'],
99
+ },
100
+ };
101
+
102
+ export async function handler(args, api) {
103
+ // Build params — serialize interactions to JSON string for the API
104
+ const params = { ...args };
105
+ if (Array.isArray(params.interactions)) {
106
+ params.interactions = JSON.stringify(params.interactions);
107
+ }
108
+
109
+ const accepted = await api.glance(params);
110
+ const jobId = accepted.job;
111
+
112
+ // Poll for result
113
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
114
+ while (Date.now() < deadline) {
115
+ await sleep(POLL_INTERVAL_MS);
116
+ const res = await api.result(jobId);
117
+
118
+ if (res.status === 'done') {
119
+ const lines = [`Capture complete for ${args.url}`, ''];
120
+ const screenshotEntries = [];
121
+
122
+ if (res.artifacts) {
123
+ const { meta, ...urlArtifacts } = res.artifacts;
124
+
125
+ for (const [key, value] of Object.entries(urlArtifacts)) {
126
+ if (typeof value === 'string' && value.endsWith('.png')) {
127
+ screenshotEntries.push([key, value]);
128
+ } else if (typeof value === 'string') {
129
+ lines.push(`${key}: ${value}`);
130
+ }
131
+ }
132
+
133
+ // Include screenshot URLs in text so agents can link/embed them
134
+ for (const [key, url] of screenshotEntries) {
135
+ lines.push(`${key}: ${url}`);
136
+ }
137
+
138
+ // Inline meta
139
+ if (meta && typeof meta === 'object') {
140
+ lines.push('');
141
+ lines.push('meta: ' + JSON.stringify(meta, null, 2));
142
+ }
143
+ }
144
+
145
+ if (res.capture_url) {
146
+ lines.push(`capture_url: ${res.capture_url}`);
147
+ }
148
+
149
+ if (res.usage) {
150
+ lines.push('');
151
+ lines.push(`Usage: ${res.usage.used} used, ${res.usage.remaining} remaining`);
152
+ }
153
+
154
+ const content = [{ type: 'text', text: lines.join('\n') }];
155
+
156
+ // Fetch screenshots and return as inline image blocks
157
+ for (const [key, url] of screenshotEntries) {
158
+ try {
159
+ const buf = await api.fetchImage(url);
160
+ content.push({ type: 'image', data: buf.toString('base64'), mimeType: 'image/png' });
161
+ } catch {
162
+ // If fetch fails, include the URL as text instead
163
+ content.push({ type: 'text', text: `${key}: ${url} (image fetch failed)` });
164
+ }
165
+ }
166
+
167
+ return { content };
168
+ }
169
+
170
+ if (res.status === 'failed') {
171
+ return errorResult(
172
+ `Capture failed for ${args.url}: ${res.error || 'unknown'}` +
173
+ (res.detail ? ` — ${res.detail}` : '')
174
+ );
175
+ }
176
+ }
177
+
178
+ return errorResult(`Capture timed out after ${POLL_TIMEOUT_MS / 1000}s for ${args.url} (job: ${jobId}).`);
179
+ }
package/src/tools.mjs DELETED
@@ -1,323 +0,0 @@
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
- const screenshotEntries = [];
212
-
213
- if (res.artifacts) {
214
- const { meta, ...urlArtifacts } = res.artifacts;
215
-
216
- for (const [key, value] of Object.entries(urlArtifacts)) {
217
- if (typeof value === 'string' && value.endsWith('.png')) {
218
- screenshotEntries.push([key, value]);
219
- } else if (typeof value === 'string') {
220
- lines.push(`${key}: ${value}`);
221
- }
222
- }
223
-
224
- // Inline meta
225
- if (meta && typeof meta === 'object') {
226
- lines.push('');
227
- lines.push('meta: ' + JSON.stringify(meta, null, 2));
228
- }
229
- }
230
-
231
- if (res.usage) {
232
- lines.push('');
233
- lines.push(`Usage: ${res.usage.used} used, ${res.usage.remaining} remaining`);
234
- }
235
-
236
- const content = [{ type: 'text', text: lines.join('\n') }];
237
-
238
- // Fetch screenshots and return as inline image blocks
239
- for (const [key, url] of screenshotEntries) {
240
- try {
241
- const buf = await api.fetchImage(url);
242
- content.push({ type: 'image', data: buf.toString('base64'), mimeType: 'image/png' });
243
- } catch {
244
- // If fetch fails, include the URL as text instead
245
- content.push({ type: 'text', text: `${key}: ${url} (image fetch failed)` });
246
- }
247
- }
248
-
249
- return { content };
250
- }
251
-
252
- if (res.status === 'failed') {
253
- return errorResult(
254
- `Capture failed for ${args.url}: ${res.error || 'unknown'}` +
255
- (res.detail ? ` — ${res.detail}` : '')
256
- );
257
- }
258
- }
259
-
260
- return errorResult(`Capture timed out after ${POLL_TIMEOUT_MS / 1000}s for ${args.url} (job: ${jobId}).`);
261
- }
262
-
263
- async function handleUsage(args, api) {
264
- const res = await api.usage();
265
- const lines = [
266
- `Tier: ${res.tier}`,
267
- `Used: ${res.used} / ${res.calls_per_month}`,
268
- `Running: ${res.running}`,
269
- `Resets at: ${res.resets_at}`,
270
- ];
271
- return textResult(lines.join('\n'));
272
- }
273
-
274
- async function handleFeedback(args, api) {
275
- await api.feedback(args.message);
276
- return textResult('Feedback received. Thank you.');
277
- }
278
-
279
- async function handleHello(args, api) {
280
- const res = await api.hello();
281
- const lines = [
282
- res.glance,
283
- '',
284
- `Version: ${res.version}`,
285
- `Capabilities: ${res.capabilities.join(', ')}`,
286
- `Free tier: ${res.free_tier.calls_per_month} calls/month`,
287
- `Methods: ${res.methods.join(', ')}`,
288
- ];
289
- return textResult(lines.join('\n'));
290
- }
291
-
292
- async function handleSignup(args, api) {
293
- const res = await api.signup(args.email);
294
- api.setApiKey(res.api_key);
295
- return textResult(
296
- `Account ready. Tier: ${res.tier}, ${res.calls_per_month} calls/month. ` +
297
- 'API key is active for this session.'
298
- );
299
- }
300
-
301
- const handlers = {
302
- glance: handleGlance,
303
- usage: handleUsage,
304
- feedback: handleFeedback,
305
- hello: handleHello,
306
- signup: handleSignup,
307
- };
308
-
309
- export async function handleToolCall(name, args, api) {
310
- const handler = handlers[name];
311
- if (!handler) {
312
- return errorResult(`Unknown tool: ${name}`);
313
- }
314
- try {
315
- return await handler(args || {}, api);
316
- } catch (err) {
317
- if (err.code && err.code <= -32000) {
318
- return formatApiError(err);
319
- }
320
- // Network / unexpected errors
321
- return errorResult(`API request failed: ${err.message}`);
322
- }
323
- }