iframer-cli 1.0.4 → 2.0.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.
- package/{cli.cjs → cli.js} +32 -105
- package/mcp-server.cjs +28715 -0
- package/package.json +7 -25
- package/bun.lock +0 -195
- package/index.js +0 -633
package/index.js
DELETED
|
@@ -1,633 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { readFileSync } from "fs";
|
|
6
|
-
import { join } from "path";
|
|
7
|
-
import { homedir } from "os";
|
|
8
|
-
|
|
9
|
-
// ─── Config ──────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
const BASE_URL = process.env.IFRAMER_URL || "https://api.iframer.sh";
|
|
12
|
-
const CREDENTIALS_PATH = join(homedir(), ".iframer", "credentials.json");
|
|
13
|
-
|
|
14
|
-
let cachedToken = null;
|
|
15
|
-
|
|
16
|
-
function loadToken() {
|
|
17
|
-
if (cachedToken) return cachedToken;
|
|
18
|
-
try {
|
|
19
|
-
const data = JSON.parse(readFileSync(CREDENTIALS_PATH, "utf8"));
|
|
20
|
-
cachedToken = data.token;
|
|
21
|
-
return cachedToken;
|
|
22
|
-
} catch {
|
|
23
|
-
throw new Error("Not logged in. Run: iframer login");
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function authHeaders() {
|
|
28
|
-
return {
|
|
29
|
-
"Content-Type": "application/json",
|
|
30
|
-
Authorization: `Bearer ${loadToken()}`,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function apiPost(endpoint, body) {
|
|
35
|
-
const res = await fetch(`${BASE_URL}${endpoint}`, {
|
|
36
|
-
method: "POST",
|
|
37
|
-
headers: authHeaders(),
|
|
38
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
39
|
-
});
|
|
40
|
-
return res.json();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function apiGet(endpoint) {
|
|
44
|
-
const res = await fetch(`${BASE_URL}${endpoint}`, { headers: authHeaders() });
|
|
45
|
-
return res.json();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function apiDelete(endpoint) {
|
|
49
|
-
const res = await fetch(`${BASE_URL}${endpoint}`, {
|
|
50
|
-
method: "DELETE",
|
|
51
|
-
headers: authHeaders(),
|
|
52
|
-
});
|
|
53
|
-
return res.json();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function errorResponse(message) {
|
|
57
|
-
return { content: [{ type: "text", text: message }], isError: true };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ─── MCP Server ──────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
const server = new McpServer({
|
|
63
|
-
name: "iframer",
|
|
64
|
-
version: "1.0.0",
|
|
65
|
-
instructions: `iframer — a headful/headless browser for AI agents.
|
|
66
|
-
|
|
67
|
-
CRITICAL: NEVER use the CLI (bin/cli.js) or bash commands. NEVER tell the user to run CLI commands. Use ONLY these MCP tools. Everything works through MCP — no bash, no curl, no CLI.
|
|
68
|
-
|
|
69
|
-
WORKFLOW:
|
|
70
|
-
1. Call "status" FIRST to check API, auth, sessions, and credentials.
|
|
71
|
-
2. For simple page fetches, use "browse" (headless, fast, cheap).
|
|
72
|
-
3. For anything requiring vision (CAPTCHAs, login forms, 2FA), use "interactive_start" then "interactive_act".
|
|
73
|
-
4. Screenshots are returned as URLs. Use Read tool to view them ONLY when you need to see the page.
|
|
74
|
-
5. Skip screenshots (screenshot: false) when you already know what to click.
|
|
75
|
-
6. For logins: check credentials with "status". If credentials are missing, call "store_credentials" to prompt the user — NEVER tell them to use the CLI. Then use "login" tool.
|
|
76
|
-
7. Always call "interactive_stop" when done to save session state.
|
|
77
|
-
|
|
78
|
-
CAPTCHA STRATEGY (escalation order):
|
|
79
|
-
1. First try headless — many sites use invisible reCAPTCHA v3 that passes without a challenge.
|
|
80
|
-
2. If a challenge appears, use recaptcha_solve + recaptcha_answer to solve it visually.
|
|
81
|
-
3. If challenges keep looping (3+ attempts), the server IP is flagged. Tell the user to run "iframer proxy" in a separate terminal to route traffic through their residential IP. This is the ONLY CLI command you may suggest. Check proxy status with the "status" tool.
|
|
82
|
-
|
|
83
|
-
MINIMIZE TOKEN USAGE: Don't screenshot every action. Only look when you need to decide what to do next.
|
|
84
|
-
NEVER use bash/CLI (except "iframer proxy" when CAPTCHA fails). ALL interaction goes through these MCP tools.`,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// ─── Tool 0: status (call this first) ────────────────────────────────
|
|
88
|
-
|
|
89
|
-
server.tool(
|
|
90
|
-
"status",
|
|
91
|
-
`Get the full state of iframer in one call. CALL THIS FIRST before doing anything else. Returns: API health, auth status, active sessions, stored credentials. This eliminates the need for discovery.`,
|
|
92
|
-
{},
|
|
93
|
-
async () => {
|
|
94
|
-
try {
|
|
95
|
-
const status = { api: false, authenticated: false, session: null, credentials: [] };
|
|
96
|
-
|
|
97
|
-
// Check API health
|
|
98
|
-
try {
|
|
99
|
-
const health = await fetch(`${BASE_URL}/health`);
|
|
100
|
-
const data = await health.json();
|
|
101
|
-
status.api = data.ok === true;
|
|
102
|
-
} catch {
|
|
103
|
-
return { content: [{ type: "text", text: `API not running at ${BASE_URL}. Start it with: bun run start:docker` }], isError: true };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Check auth
|
|
107
|
-
try {
|
|
108
|
-
loadToken();
|
|
109
|
-
status.authenticated = true;
|
|
110
|
-
} catch {
|
|
111
|
-
return { content: [{ type: "text", text: JSON.stringify({ ...status, authenticated: false, message: "Not logged in. Run: iframer login" }, null, 2) }] };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Check interactive session
|
|
115
|
-
try {
|
|
116
|
-
const sessionData = await apiGet("/interactive/status");
|
|
117
|
-
if (sessionData.ok && sessionData.active) {
|
|
118
|
-
status.session = { active: true, noVncUrl: sessionData.noVncUrl, createdAt: sessionData.createdAt };
|
|
119
|
-
} else {
|
|
120
|
-
status.session = { active: false };
|
|
121
|
-
}
|
|
122
|
-
} catch {}
|
|
123
|
-
|
|
124
|
-
// Check stored credentials
|
|
125
|
-
try {
|
|
126
|
-
const credData = await apiGet("/credentials");
|
|
127
|
-
if (credData.ok) status.credentials = credData.domains;
|
|
128
|
-
} catch {}
|
|
129
|
-
|
|
130
|
-
// Check proxy tunnel
|
|
131
|
-
try {
|
|
132
|
-
const proxyData = await apiGet("/proxy/status");
|
|
133
|
-
if (proxyData.ok) status.proxyTunnel = proxyData.active;
|
|
134
|
-
} catch {}
|
|
135
|
-
|
|
136
|
-
return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
|
|
137
|
-
} catch (err) {
|
|
138
|
-
return errorResponse(`Error: ${err.message}`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
// ─── Tool 1: browse ──────────────────────────────────────────────────
|
|
144
|
-
|
|
145
|
-
server.tool(
|
|
146
|
-
"browse",
|
|
147
|
-
"Fetch a web page using a headless browser. Renders JavaScript, can execute actions (click, fill, scroll), and extract data. Session cookies persist across calls.",
|
|
148
|
-
{
|
|
149
|
-
url: z.string().describe("URL to navigate to"),
|
|
150
|
-
extract: z.string().optional().describe("JavaScript expression to evaluate on the page (e.g. 'document.title')"),
|
|
151
|
-
actions: z.array(z.object({
|
|
152
|
-
type: z.enum(["click", "fill", "wait", "scroll", "human-click", "human-type"]),
|
|
153
|
-
selector: z.string().optional(),
|
|
154
|
-
value: z.string().optional(),
|
|
155
|
-
ms: z.number().optional(),
|
|
156
|
-
})).optional().describe("Actions to execute before extracting"),
|
|
157
|
-
returnHtml: z.boolean().optional().describe("Return full page HTML"),
|
|
158
|
-
waitForSelector: z.string().optional().describe("Wait for this CSS selector before proceeding"),
|
|
159
|
-
sessionless: z.boolean().optional().describe("Skip session persistence for this request"),
|
|
160
|
-
},
|
|
161
|
-
async (params) => {
|
|
162
|
-
try {
|
|
163
|
-
const data = await apiPost("/fetch", params);
|
|
164
|
-
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
165
|
-
const { html, ...rest } = data;
|
|
166
|
-
const text = html
|
|
167
|
-
? JSON.stringify(rest, null, 2) + "\n\n--- HTML ---\n" + html
|
|
168
|
-
: JSON.stringify(rest, null, 2);
|
|
169
|
-
return { content: [{ type: "text", text }] };
|
|
170
|
-
} catch (err) {
|
|
171
|
-
return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
// ─── Tool 2: interactive_start ───────────────────────────────────────
|
|
177
|
-
|
|
178
|
-
server.tool(
|
|
179
|
-
"interactive_start",
|
|
180
|
-
"Start an interactive headful browser session. Opens a real Chromium window (streamed via noVNC). Use this when you need to see and interact with the page — CAPTCHAs, 2FA, login flows, etc.",
|
|
181
|
-
{
|
|
182
|
-
url: z.string().optional().describe("URL to navigate to on start"),
|
|
183
|
-
},
|
|
184
|
-
async (params) => {
|
|
185
|
-
try {
|
|
186
|
-
const data = await apiPost("/interactive/start", params);
|
|
187
|
-
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
188
|
-
return {
|
|
189
|
-
content: [{
|
|
190
|
-
type: "text",
|
|
191
|
-
text: `Interactive session started.\nnoVNC viewer: ${data.noVncUrl}\n\nUse interactive_screenshot to see the page, interactive_act to interact with it.`,
|
|
192
|
-
}],
|
|
193
|
-
};
|
|
194
|
-
} catch (err) {
|
|
195
|
-
return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
// ─── Tool 3: interactive_screenshot ──────────────────────────────────
|
|
201
|
-
|
|
202
|
-
server.tool(
|
|
203
|
-
"interactive_screenshot",
|
|
204
|
-
"Take a screenshot of the current interactive browser session. Returns a URL to the image — use Read tool or WebFetch to view it.",
|
|
205
|
-
{},
|
|
206
|
-
async () => {
|
|
207
|
-
try {
|
|
208
|
-
const data = await apiGet("/interactive/screenshot");
|
|
209
|
-
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
210
|
-
return {
|
|
211
|
-
content: [{
|
|
212
|
-
type: "text",
|
|
213
|
-
text: `Page: ${data.title}\nURL: ${data.url}\nScreenshot: ${data.screenshotUrl}`,
|
|
214
|
-
}],
|
|
215
|
-
};
|
|
216
|
-
} catch (err) {
|
|
217
|
-
return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
// ─── Tool 4: interactive_act ─────────────────────────────────────────
|
|
223
|
-
|
|
224
|
-
server.tool(
|
|
225
|
-
"interactive_act",
|
|
226
|
-
`Send an action to the interactive browser and get a screenshot URL of the result.
|
|
227
|
-
|
|
228
|
-
Actions: click, human-click, human-type, fill, navigate, scroll, wait, evaluate, wait-for-selector, keyboard, type-code.
|
|
229
|
-
type-code: Type a verification code into split digit inputs in one call. Pass the full code as value (e.g. "735948"). Optionally pass selector for the first input (defaults to input[type="tel"]).
|
|
230
|
-
reCAPTCHA: recaptcha-click (click checkbox), recaptcha-select (select tiles by index), recaptcha-verify (click verify), recaptcha-info (get challenge metadata).
|
|
231
|
-
|
|
232
|
-
Screenshots are saved as files and returned as URLs. Use Read tool to view them. For reCAPTCHA, individual tile image URLs are returned.`,
|
|
233
|
-
{
|
|
234
|
-
action: z.enum([
|
|
235
|
-
"click", "human-click", "human-type", "fill",
|
|
236
|
-
"navigate", "scroll", "wait", "evaluate",
|
|
237
|
-
"wait-for-selector", "keyboard", "type-code",
|
|
238
|
-
"recaptcha-click", "recaptcha-select", "recaptcha-verify", "recaptcha-info",
|
|
239
|
-
]).describe("Action type to perform"),
|
|
240
|
-
selector: z.string().optional().describe("CSS selector (for click, fill, human-click, human-type, wait-for-selector)"),
|
|
241
|
-
value: z.string().optional().describe("Value to type (for fill, human-type)"),
|
|
242
|
-
url: z.string().optional().describe("URL for navigate action"),
|
|
243
|
-
x: z.number().optional().describe("X coordinate for human-click"),
|
|
244
|
-
y: z.number().optional().describe("Y coordinate for human-click"),
|
|
245
|
-
deltaY: z.number().optional().describe("Scroll pixels (default: full page)"),
|
|
246
|
-
ms: z.number().optional().describe("Milliseconds for wait action"),
|
|
247
|
-
expression: z.string().optional().describe("JavaScript for evaluate action"),
|
|
248
|
-
key: z.string().optional().describe("Key for keyboard action (e.g. Enter, Tab)"),
|
|
249
|
-
tiles: z.array(z.number()).optional().describe("Tile indices for recaptcha-select (e.g. [0, 2, 5])"),
|
|
250
|
-
timeout: z.number().optional().describe("Timeout in ms for wait-for-selector"),
|
|
251
|
-
screenshot: z.boolean().optional().describe("Set to false to skip screenshot (faster)"),
|
|
252
|
-
},
|
|
253
|
-
async (params) => {
|
|
254
|
-
try {
|
|
255
|
-
const { action: actionType, screenshot: wantScreenshot, ...rest } = params;
|
|
256
|
-
const actionObj = { type: actionType };
|
|
257
|
-
|
|
258
|
-
for (const [key, val] of Object.entries(rest)) {
|
|
259
|
-
if (val !== undefined) actionObj[key] = val;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const data = await apiPost("/interactive/act", { action: actionObj, screenshot: wantScreenshot });
|
|
263
|
-
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
264
|
-
|
|
265
|
-
const textData = { url: data.url, title: data.title };
|
|
266
|
-
if (data.result !== null && data.result !== undefined) textData.result = data.result;
|
|
267
|
-
if (data.screenshotUrl) textData.screenshotUrl = data.screenshotUrl;
|
|
268
|
-
|
|
269
|
-
// Include tile URLs for reCAPTCHA
|
|
270
|
-
if (data.tileUrls && data.tileUrls.length > 0) {
|
|
271
|
-
textData.tileUrls = data.tileUrls;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return { content: [{ type: "text", text: JSON.stringify(textData, null, 2) }] };
|
|
275
|
-
} catch (err) {
|
|
276
|
-
return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
// ─── Tool 4b: interactive_batch ──────────────────────────────────────
|
|
282
|
-
|
|
283
|
-
server.tool(
|
|
284
|
-
"interactive_batch",
|
|
285
|
-
`Run multiple actions in a single call. Much faster than calling interactive_act repeatedly.
|
|
286
|
-
Each action is an object with a "type" and its parameters. Actions run sequentially. Stops on first error unless continueOnError is true.
|
|
287
|
-
Only one screenshot is taken at the end (or none if screenshot: false).
|
|
288
|
-
|
|
289
|
-
Example: [
|
|
290
|
-
{"type": "navigate", "url": "https://example.com"},
|
|
291
|
-
{"type": "wait", "ms": 2000},
|
|
292
|
-
{"type": "click", "selector": "#login"},
|
|
293
|
-
{"type": "evaluate", "expression": "document.title"}
|
|
294
|
-
]`,
|
|
295
|
-
{
|
|
296
|
-
actions: z.array(z.object({
|
|
297
|
-
type: z.string().describe("Action type"),
|
|
298
|
-
selector: z.string().optional(),
|
|
299
|
-
value: z.string().optional(),
|
|
300
|
-
url: z.string().optional(),
|
|
301
|
-
x: z.number().optional(),
|
|
302
|
-
y: z.number().optional(),
|
|
303
|
-
deltaY: z.number().optional(),
|
|
304
|
-
ms: z.number().optional(),
|
|
305
|
-
expression: z.string().optional(),
|
|
306
|
-
key: z.string().optional(),
|
|
307
|
-
tiles: z.array(z.number()).optional(),
|
|
308
|
-
timeout: z.number().optional(),
|
|
309
|
-
waitUntil: z.string().optional(),
|
|
310
|
-
})).describe("Array of actions to execute sequentially"),
|
|
311
|
-
screenshot: z.boolean().optional().describe("Take screenshot at the end (default true)"),
|
|
312
|
-
continueOnError: z.boolean().optional().describe("Continue executing actions even if one fails"),
|
|
313
|
-
},
|
|
314
|
-
async (params) => {
|
|
315
|
-
try {
|
|
316
|
-
const data = await apiPost("/interactive/batch", params);
|
|
317
|
-
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
318
|
-
|
|
319
|
-
const textData = {
|
|
320
|
-
url: data.url,
|
|
321
|
-
title: data.title,
|
|
322
|
-
results: data.results,
|
|
323
|
-
};
|
|
324
|
-
if (data.screenshotUrl) textData.screenshotUrl = data.screenshotUrl;
|
|
325
|
-
|
|
326
|
-
return { content: [{ type: "text", text: JSON.stringify(textData, null, 2) }] };
|
|
327
|
-
} catch (err) {
|
|
328
|
-
return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
// ─── Tool 5: interactive_stop ────────────────────────────────────────
|
|
334
|
-
|
|
335
|
-
server.tool(
|
|
336
|
-
"interactive_stop",
|
|
337
|
-
"Stop the interactive browser session and save session state (cookies, localStorage) for future use.",
|
|
338
|
-
{},
|
|
339
|
-
async () => {
|
|
340
|
-
try {
|
|
341
|
-
const data = await apiPost("/interactive/stop");
|
|
342
|
-
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
343
|
-
return {
|
|
344
|
-
content: [{ type: "text", text: `Session stopped. State saved: ${data.sessionSaved}` }],
|
|
345
|
-
};
|
|
346
|
-
} catch (err) {
|
|
347
|
-
return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
// ─── Tool 6: interactive_status ──────────────────────────────────────
|
|
353
|
-
|
|
354
|
-
server.tool(
|
|
355
|
-
"interactive_status",
|
|
356
|
-
"Check if an interactive browser session is currently active.",
|
|
357
|
-
{},
|
|
358
|
-
async () => {
|
|
359
|
-
try {
|
|
360
|
-
const data = await apiGet("/interactive/status");
|
|
361
|
-
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
362
|
-
if (!data.active) {
|
|
363
|
-
return { content: [{ type: "text", text: "No active interactive session." }] };
|
|
364
|
-
}
|
|
365
|
-
return {
|
|
366
|
-
content: [{
|
|
367
|
-
type: "text",
|
|
368
|
-
text: `Active session\nnoVNC: ${data.noVncUrl}\nStarted: ${data.createdAt}`,
|
|
369
|
-
}],
|
|
370
|
-
};
|
|
371
|
-
} catch (err) {
|
|
372
|
-
return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
// ─── Tool 7: clear_session ───────────────────────────────────────────
|
|
378
|
-
|
|
379
|
-
server.tool(
|
|
380
|
-
"clear_session",
|
|
381
|
-
"Clear all stored browser session data (cookies, localStorage). Start fresh on next browse.",
|
|
382
|
-
{},
|
|
383
|
-
async () => {
|
|
384
|
-
try {
|
|
385
|
-
const data = await apiDelete("/session");
|
|
386
|
-
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
387
|
-
return { content: [{ type: "text", text: "Session cleared." }] };
|
|
388
|
-
} catch (err) {
|
|
389
|
-
return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
);
|
|
393
|
-
|
|
394
|
-
// ─── Tool 8: login ──────────────────────────────────────────────────
|
|
395
|
-
|
|
396
|
-
server.tool(
|
|
397
|
-
"login",
|
|
398
|
-
`Log into a website using stored credentials. The server handles the login — passwords never enter the AI context.
|
|
399
|
-
|
|
400
|
-
Users must store credentials first via store_credentials tool or CLI.
|
|
401
|
-
|
|
402
|
-
The agent provides CSS selectors for the login form fields. The server decrypts credentials internally, fills the form with human-like typing, and clicks submit. If TOTP 2FA is configured, it generates and enters the code automatically.
|
|
403
|
-
|
|
404
|
-
IMPORTANT: You will never see the actual credentials. You only need to identify the form selectors on the page.`,
|
|
405
|
-
{
|
|
406
|
-
domain: z.string().describe("Domain to log into (must match stored credentials, e.g. 'github.com')"),
|
|
407
|
-
usernameSelector: z.string().optional().describe("CSS selector for the username/email input field"),
|
|
408
|
-
passwordSelector: z.string().optional().describe("CSS selector for the password input field"),
|
|
409
|
-
submitSelector: z.string().optional().describe("CSS selector for the submit/login button"),
|
|
410
|
-
totpSelector: z.string().optional().describe("CSS selector for the TOTP/2FA code input (if applicable)"),
|
|
411
|
-
},
|
|
412
|
-
async (params) => {
|
|
413
|
-
try {
|
|
414
|
-
const data = await apiPost("/credentials/login", params);
|
|
415
|
-
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
416
|
-
|
|
417
|
-
const result = {
|
|
418
|
-
message: data.message,
|
|
419
|
-
totpGenerated: data.totpGenerated,
|
|
420
|
-
url: data.url,
|
|
421
|
-
title: data.title,
|
|
422
|
-
};
|
|
423
|
-
if (data.screenshotUrl) result.screenshotUrl = data.screenshotUrl;
|
|
424
|
-
|
|
425
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
426
|
-
} catch (err) {
|
|
427
|
-
return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
// ─── Tool 9: list_credentials ───────────────────────────────────────
|
|
433
|
-
|
|
434
|
-
server.tool(
|
|
435
|
-
"list_credentials",
|
|
436
|
-
"List domains for which the user has stored login credentials. Returns only domain names, never the actual credential values.",
|
|
437
|
-
{},
|
|
438
|
-
async () => {
|
|
439
|
-
try {
|
|
440
|
-
const data = await apiGet("/credentials");
|
|
441
|
-
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
442
|
-
if (data.domains.length === 0) {
|
|
443
|
-
return { content: [{ type: "text", text: "No credentials stored. User can add them with: iframer credentials add <domain>" }] };
|
|
444
|
-
}
|
|
445
|
-
return {
|
|
446
|
-
content: [{ type: "text", text: `Stored credentials for:\n${data.domains.map(d => ` - ${d}`).join("\n")}` }],
|
|
447
|
-
};
|
|
448
|
-
} catch (err) {
|
|
449
|
-
return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
// ─── Tool 10: store_credentials ─────────────────────────────────────
|
|
455
|
-
|
|
456
|
-
server.tool(
|
|
457
|
-
"store_credentials",
|
|
458
|
-
`Store login credentials for a website. Prompts the user directly for their username, password, and optional TOTP secret. Credentials are encrypted and stored server-side — the AI never sees them.
|
|
459
|
-
|
|
460
|
-
ALWAYS call this tool when credentials are needed but not stored. NEVER tell the user to run CLI commands — this tool handles it via a secure prompt.`,
|
|
461
|
-
{
|
|
462
|
-
domain: z.string().describe("Domain to store credentials for (e.g. 'github.com', 'mercadolivre.com.br')"),
|
|
463
|
-
},
|
|
464
|
-
async ({ domain }) => {
|
|
465
|
-
try {
|
|
466
|
-
const result = await server.server.elicitInput({
|
|
467
|
-
mode: "form",
|
|
468
|
-
message: `Enter your login credentials for ${domain}.\nThese will be encrypted and stored securely. The AI will never see them.`,
|
|
469
|
-
requestedSchema: {
|
|
470
|
-
type: "object",
|
|
471
|
-
properties: {
|
|
472
|
-
username: {
|
|
473
|
-
type: "string",
|
|
474
|
-
title: "Username / Email",
|
|
475
|
-
description: "Your username or email for this site",
|
|
476
|
-
minLength: 1,
|
|
477
|
-
},
|
|
478
|
-
password: {
|
|
479
|
-
type: "string",
|
|
480
|
-
title: "Password",
|
|
481
|
-
description: "Your password",
|
|
482
|
-
minLength: 1,
|
|
483
|
-
},
|
|
484
|
-
totp_secret: {
|
|
485
|
-
type: "string",
|
|
486
|
-
title: "TOTP Secret (optional)",
|
|
487
|
-
description: "Your authenticator app secret key for automatic 2FA. Leave empty if not using TOTP.",
|
|
488
|
-
},
|
|
489
|
-
},
|
|
490
|
-
required: ["username", "password"],
|
|
491
|
-
},
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
if (result.action === "decline" || !result.content) {
|
|
495
|
-
return { content: [{ type: "text", text: "Credential storage cancelled by user." }] };
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
const { username, password, totp_secret } = result.content;
|
|
499
|
-
const data = await apiPost("/credentials", {
|
|
500
|
-
domain,
|
|
501
|
-
username,
|
|
502
|
-
password,
|
|
503
|
-
totp_secret: totp_secret || undefined,
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
507
|
-
|
|
508
|
-
return {
|
|
509
|
-
content: [{
|
|
510
|
-
type: "text",
|
|
511
|
-
text: `Credentials stored securely for ${domain}. Use the login tool to log in.`,
|
|
512
|
-
}],
|
|
513
|
-
};
|
|
514
|
-
} catch (err) {
|
|
515
|
-
if (err.message?.includes("does not support")) {
|
|
516
|
-
return {
|
|
517
|
-
content: [{
|
|
518
|
-
type: "text",
|
|
519
|
-
text: `This client doesn't support secure input prompts. Please store credentials via the CLI instead:\n\niframer credentials add ${domain}`,
|
|
520
|
-
}],
|
|
521
|
-
isError: true,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
return errorResponse(`Error: ${err.message}`);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
);
|
|
528
|
-
|
|
529
|
-
// ─── Tool 11: recaptcha_solve ───────────────────────────────────────
|
|
530
|
-
|
|
531
|
-
server.tool(
|
|
532
|
-
"recaptcha_solve",
|
|
533
|
-
`Start solving a reCAPTCHA. Clicks the checkbox and if a challenge appears, returns ALL tile images inline in one response along with the prompt text. Much faster than individual recaptcha-click + screenshot + recaptcha-info calls.
|
|
534
|
-
|
|
535
|
-
If the reCAPTCHA is solved immediately (no challenge), returns solved: true.
|
|
536
|
-
If a challenge appears, returns the prompt and all tile images. Then use recaptcha_answer with the tile indices to complete it.`,
|
|
537
|
-
{},
|
|
538
|
-
async () => {
|
|
539
|
-
try {
|
|
540
|
-
const data = await apiPost("/interactive/act", {
|
|
541
|
-
action: { type: "recaptcha-solve" },
|
|
542
|
-
screenshot: false,
|
|
543
|
-
});
|
|
544
|
-
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
545
|
-
|
|
546
|
-
const result = data.result;
|
|
547
|
-
if (result.solved) {
|
|
548
|
-
return { content: [{ type: "text", text: "reCAPTCHA solved automatically (no challenge needed)." }] };
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Build content blocks: prompt text + all tile images
|
|
552
|
-
const content = [];
|
|
553
|
-
content.push({
|
|
554
|
-
type: "text",
|
|
555
|
-
text: `reCAPTCHA Challenge: "${result.prompt}"\nGrid: ${result.rows}x${result.cols} (${result.tiles.length} tiles)\nSelect the correct tiles by index (0-${result.tiles.length - 1}) using recaptcha_answer.`,
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
for (const tile of result.tiles) {
|
|
559
|
-
if (tile.image) {
|
|
560
|
-
content.push({
|
|
561
|
-
type: "text",
|
|
562
|
-
text: `Tile ${tile.index}:`,
|
|
563
|
-
});
|
|
564
|
-
content.push({
|
|
565
|
-
type: "image",
|
|
566
|
-
data: tile.image,
|
|
567
|
-
mimeType: "image/jpeg",
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
return { content };
|
|
573
|
-
} catch (err) {
|
|
574
|
-
return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
);
|
|
578
|
-
|
|
579
|
-
// ─── Tool 12: recaptcha_answer ──────────────────────────────────────
|
|
580
|
-
|
|
581
|
-
server.tool(
|
|
582
|
-
"recaptcha_answer",
|
|
583
|
-
`Submit tile selections and verify the reCAPTCHA in one call. Selects the specified tiles and clicks verify.
|
|
584
|
-
|
|
585
|
-
If solved, returns success. If a new challenge appears (common with reCAPTCHA), returns the new prompt and tile images inline — just call recaptcha_answer again with new selections.`,
|
|
586
|
-
{
|
|
587
|
-
tiles: z.array(z.number()).describe("Tile indices to select (e.g. [0, 3, 6])"),
|
|
588
|
-
},
|
|
589
|
-
async ({ tiles }) => {
|
|
590
|
-
try {
|
|
591
|
-
const data = await apiPost("/interactive/act", {
|
|
592
|
-
action: { type: "recaptcha-answer", tiles },
|
|
593
|
-
screenshot: false,
|
|
594
|
-
});
|
|
595
|
-
if (!data.ok) return errorResponse(`Error: ${data.error}`);
|
|
596
|
-
|
|
597
|
-
const result = data.result;
|
|
598
|
-
if (result.solved) {
|
|
599
|
-
return { content: [{ type: "text", text: "reCAPTCHA solved!" }] };
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// New challenge — return tiles inline
|
|
603
|
-
const content = [];
|
|
604
|
-
content.push({
|
|
605
|
-
type: "text",
|
|
606
|
-
text: `Not solved yet — new challenge: "${result.prompt || "unknown"}"\nGrid: ${result.rows}x${result.cols} (${result.tiles.length} tiles)\nSelect the correct tiles by index using recaptcha_answer again.`,
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
for (const tile of result.tiles) {
|
|
610
|
-
if (tile.image) {
|
|
611
|
-
content.push({
|
|
612
|
-
type: "text",
|
|
613
|
-
text: `Tile ${tile.index}:`,
|
|
614
|
-
});
|
|
615
|
-
content.push({
|
|
616
|
-
type: "image",
|
|
617
|
-
data: tile.image,
|
|
618
|
-
mimeType: "image/jpeg",
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
return { content };
|
|
624
|
-
} catch (err) {
|
|
625
|
-
return errorResponse(`Connection error: ${err.message}. Is the API running at ${BASE_URL}?`);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
// ─── Start ───────────────────────────────────────────────────────────
|
|
631
|
-
|
|
632
|
-
const transport = new StdioServerTransport();
|
|
633
|
-
await server.connect(transport);
|