auto-feedback 0.1.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 +180 -0
- package/build/capture/console-collector.d.ts +16 -0
- package/build/capture/console-collector.js +43 -0
- package/build/capture/error-collector.d.ts +15 -0
- package/build/capture/error-collector.js +47 -0
- package/build/capture/network-collector.d.ts +16 -0
- package/build/capture/network-collector.js +76 -0
- package/build/capture/process-collector.d.ts +16 -0
- package/build/capture/process-collector.js +48 -0
- package/build/capture/types.d.ts +61 -0
- package/build/capture/types.js +5 -0
- package/build/index.d.ts +6 -0
- package/build/index.js +41 -0
- package/build/interaction/selectors.d.ts +26 -0
- package/build/interaction/selectors.js +84 -0
- package/build/interaction/types.d.ts +56 -0
- package/build/interaction/types.js +5 -0
- package/build/process/cleanup.d.ts +23 -0
- package/build/process/cleanup.js +50 -0
- package/build/process/launcher.d.ts +22 -0
- package/build/process/launcher.js +54 -0
- package/build/process/monitor.d.ts +14 -0
- package/build/process/monitor.js +67 -0
- package/build/process/types.d.ts +84 -0
- package/build/process/types.js +5 -0
- package/build/screenshot/auto-capture.d.ts +14 -0
- package/build/screenshot/auto-capture.js +38 -0
- package/build/screenshot/capture.d.ts +21 -0
- package/build/screenshot/capture.js +48 -0
- package/build/screenshot/optimize.d.ts +19 -0
- package/build/screenshot/optimize.js +28 -0
- package/build/screenshot/types.d.ts +43 -0
- package/build/screenshot/types.js +4 -0
- package/build/server.d.ts +10 -0
- package/build/server.js +18 -0
- package/build/session-manager.d.ts +119 -0
- package/build/session-manager.js +284 -0
- package/build/tools/check-port.d.ts +10 -0
- package/build/tools/check-port.js +40 -0
- package/build/tools/click-element.d.ts +13 -0
- package/build/tools/click-element.js +118 -0
- package/build/tools/get-console-logs.d.ts +7 -0
- package/build/tools/get-console-logs.js +55 -0
- package/build/tools/get-element-state.d.ts +14 -0
- package/build/tools/get-element-state.js +116 -0
- package/build/tools/get-errors.d.ts +7 -0
- package/build/tools/get-errors.js +40 -0
- package/build/tools/get-network-logs.d.ts +7 -0
- package/build/tools/get-network-logs.js +58 -0
- package/build/tools/get-process-output.d.ts +7 -0
- package/build/tools/get-process-output.js +55 -0
- package/build/tools/get-screenshot.d.ts +7 -0
- package/build/tools/get-screenshot.js +32 -0
- package/build/tools/index.d.ts +9 -0
- package/build/tools/index.js +117 -0
- package/build/tools/launch-electron.d.ts +13 -0
- package/build/tools/launch-electron.js +97 -0
- package/build/tools/launch-web-server.d.ts +13 -0
- package/build/tools/launch-web-server.js +88 -0
- package/build/tools/launch-windows-exe.d.ts +13 -0
- package/build/tools/launch-windows-exe.js +81 -0
- package/build/tools/navigate.d.ts +13 -0
- package/build/tools/navigate.js +137 -0
- package/build/tools/run-workflow.d.ts +14 -0
- package/build/tools/run-workflow.js +207 -0
- package/build/tools/screenshot-desktop.d.ts +13 -0
- package/build/tools/screenshot-desktop.js +80 -0
- package/build/tools/screenshot-electron.d.ts +13 -0
- package/build/tools/screenshot-electron.js +72 -0
- package/build/tools/screenshot-web.d.ts +13 -0
- package/build/tools/screenshot-web.js +129 -0
- package/build/tools/stop-process.d.ts +14 -0
- package/build/tools/stop-process.js +41 -0
- package/build/tools/type-text.d.ts +13 -0
- package/build/tools/type-text.js +137 -0
- package/build/tools/wait-for-element.d.ts +14 -0
- package/build/tools/wait-for-element.js +93 -0
- package/build/types/index.d.ts +31 -0
- package/build/types/index.js +4 -0
- package/build/utils/errors.d.ts +26 -0
- package/build/utils/errors.js +62 -0
- package/build/utils/shutdown.d.ts +16 -0
- package/build/utils/shutdown.js +34 -0
- package/build/workflow/assertions.d.ts +25 -0
- package/build/workflow/assertions.js +326 -0
- package/build/workflow/executor.d.ts +34 -0
- package/build/workflow/executor.js +269 -0
- package/build/workflow/types.d.ts +95 -0
- package/build/workflow/types.js +6 -0
- package/package.json +36 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* screenshot_electron MCP tool
|
|
3
|
+
* Captures a screenshot of a running Electron app via stored Page reference
|
|
4
|
+
*/
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { SessionManager } from "../session-manager.js";
|
|
7
|
+
/**
|
|
8
|
+
* Register the screenshot_electron tool with the MCP server
|
|
9
|
+
*
|
|
10
|
+
* @param server - MCP server instance
|
|
11
|
+
* @param sessionManager - Session manager for resource tracking
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerScreenshotElectronTool(server: McpServer, sessionManager: SessionManager): void;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* screenshot_electron MCP tool
|
|
3
|
+
* Captures a screenshot of a running Electron app via stored Page reference
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createToolError, createScreenshotResult } from "../utils/errors.js";
|
|
7
|
+
import { capturePlaywrightPage } from "../screenshot/capture.js";
|
|
8
|
+
import { optimizeScreenshot } from "../screenshot/optimize.js";
|
|
9
|
+
/**
|
|
10
|
+
* Register the screenshot_electron tool with the MCP server
|
|
11
|
+
*
|
|
12
|
+
* @param server - MCP server instance
|
|
13
|
+
* @param sessionManager - Session manager for resource tracking
|
|
14
|
+
*/
|
|
15
|
+
export function registerScreenshotElectronTool(server, sessionManager) {
|
|
16
|
+
server.tool("screenshot_electron", "Capture a screenshot of a running Electron app. Use after launch_electron to see the current visual state.", {
|
|
17
|
+
sessionId: z.string().describe("Session ID of the Electron app"),
|
|
18
|
+
fullPage: z
|
|
19
|
+
.boolean()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Capture full page (true) or viewport only (false, default)"),
|
|
22
|
+
maxWidth: z
|
|
23
|
+
.number()
|
|
24
|
+
.int()
|
|
25
|
+
.min(100)
|
|
26
|
+
.max(3840)
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Max image width in pixels (default: 1280)"),
|
|
29
|
+
quality: z
|
|
30
|
+
.number()
|
|
31
|
+
.int()
|
|
32
|
+
.min(1)
|
|
33
|
+
.max(100)
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("WebP quality 1-100 (default: 80)"),
|
|
36
|
+
}, async ({ sessionId, fullPage, maxWidth, quality }) => {
|
|
37
|
+
try {
|
|
38
|
+
const session = sessionManager.get(sessionId);
|
|
39
|
+
if (!session) {
|
|
40
|
+
return createToolError(`Session not found: ${sessionId}`, "The session may have already been ended", "Create a session and launch an Electron app first.");
|
|
41
|
+
}
|
|
42
|
+
const pageRef = sessionManager.getPageRef(sessionId, "electron");
|
|
43
|
+
if (!pageRef) {
|
|
44
|
+
return createToolError("No Electron app found for this session", `Session ${sessionId} has no Electron page reference`, "Launch an Electron app first with launch_electron.");
|
|
45
|
+
}
|
|
46
|
+
console.error(`[screenshot_electron] Capturing for session ${sessionId}`);
|
|
47
|
+
// Capture raw PNG from Playwright Page
|
|
48
|
+
const rawBuffer = await capturePlaywrightPage(pageRef.page, {
|
|
49
|
+
fullPage: fullPage ?? false,
|
|
50
|
+
});
|
|
51
|
+
// Optimize: resize + WebP conversion
|
|
52
|
+
const optimized = await optimizeScreenshot(rawBuffer, {
|
|
53
|
+
maxWidth: maxWidth ?? 1280,
|
|
54
|
+
quality: quality ?? 80,
|
|
55
|
+
});
|
|
56
|
+
const imageBase64 = optimized.data.toString("base64");
|
|
57
|
+
return createScreenshotResult({
|
|
58
|
+
sessionId,
|
|
59
|
+
type: "electron",
|
|
60
|
+
mode: fullPage ? "full-page" : "viewport",
|
|
61
|
+
width: optimized.width,
|
|
62
|
+
height: optimized.height,
|
|
63
|
+
originalSize: rawBuffer.length,
|
|
64
|
+
optimizedSize: optimized.data.length,
|
|
65
|
+
}, imageBase64, optimized.mimeType);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
return createToolError("Failed to capture Electron screenshot", message, "Ensure the Electron app is still running and the window is visible.");
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* screenshot_web MCP tool
|
|
3
|
+
* Captures a screenshot of a web page by URL with lazy browser creation
|
|
4
|
+
*/
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { SessionManager } from "../session-manager.js";
|
|
7
|
+
/**
|
|
8
|
+
* Register the screenshot_web tool with the MCP server
|
|
9
|
+
*
|
|
10
|
+
* @param server - MCP server instance
|
|
11
|
+
* @param sessionManager - Session manager for resource tracking
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerScreenshotWebTool(server: McpServer, sessionManager: SessionManager): void;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* screenshot_web MCP tool
|
|
3
|
+
* Captures a screenshot of a web page by URL with lazy browser creation
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { chromium } from "playwright";
|
|
7
|
+
import { createToolError, createScreenshotResult } from "../utils/errors.js";
|
|
8
|
+
import { capturePlaywrightPage } from "../screenshot/capture.js";
|
|
9
|
+
import { optimizeScreenshot } from "../screenshot/optimize.js";
|
|
10
|
+
import { setupAutoCapture } from "../screenshot/auto-capture.js";
|
|
11
|
+
import { attachConsoleCollector } from "../capture/console-collector.js";
|
|
12
|
+
import { attachErrorCollector } from "../capture/error-collector.js";
|
|
13
|
+
import { attachNetworkCollector } from "../capture/network-collector.js";
|
|
14
|
+
/**
|
|
15
|
+
* Register the screenshot_web tool with the MCP server
|
|
16
|
+
*
|
|
17
|
+
* @param server - MCP server instance
|
|
18
|
+
* @param sessionManager - Session manager for resource tracking
|
|
19
|
+
*/
|
|
20
|
+
export function registerScreenshotWebTool(server, sessionManager) {
|
|
21
|
+
server.tool("screenshot_web", "Capture a screenshot of a web page by URL. Creates a browser automatically on first use. Use to see visual state of web apps.", {
|
|
22
|
+
sessionId: z
|
|
23
|
+
.string()
|
|
24
|
+
.describe("Session ID to associate the browser with"),
|
|
25
|
+
url: z
|
|
26
|
+
.string()
|
|
27
|
+
.url()
|
|
28
|
+
.describe("URL to screenshot (e.g., http://localhost:3000)"),
|
|
29
|
+
fullPage: z
|
|
30
|
+
.boolean()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Capture full page (true) or viewport only (false, default)"),
|
|
33
|
+
maxWidth: z
|
|
34
|
+
.number()
|
|
35
|
+
.int()
|
|
36
|
+
.min(100)
|
|
37
|
+
.max(3840)
|
|
38
|
+
.optional()
|
|
39
|
+
.describe("Max image width in pixels (default: 1280)"),
|
|
40
|
+
quality: z
|
|
41
|
+
.number()
|
|
42
|
+
.int()
|
|
43
|
+
.min(1)
|
|
44
|
+
.max(100)
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("WebP quality 1-100 (default: 80)"),
|
|
47
|
+
}, async ({ sessionId, url, fullPage, maxWidth, quality }) => {
|
|
48
|
+
try {
|
|
49
|
+
const session = sessionManager.get(sessionId);
|
|
50
|
+
if (!session) {
|
|
51
|
+
return createToolError(`Session not found: ${sessionId}`, "The session may have already been ended", "Create a session first with create_session.");
|
|
52
|
+
}
|
|
53
|
+
// Check for existing page reference for this URL
|
|
54
|
+
let pageRef = sessionManager.getPageRef(sessionId, url);
|
|
55
|
+
if (!pageRef) {
|
|
56
|
+
// Lazy browser creation: first screenshot of this URL
|
|
57
|
+
console.error(`[screenshot_web] Creating browser for ${url} in session ${sessionId}`);
|
|
58
|
+
const browser = await chromium.launch({ headless: true });
|
|
59
|
+
const context = await browser.newContext({
|
|
60
|
+
viewport: { width: 1280, height: 720 },
|
|
61
|
+
});
|
|
62
|
+
const page = await context.newPage();
|
|
63
|
+
await page.goto(url, { waitUntil: "load", timeout: 30000 });
|
|
64
|
+
// Store page reference for reuse
|
|
65
|
+
pageRef = {
|
|
66
|
+
type: "web",
|
|
67
|
+
page,
|
|
68
|
+
browser,
|
|
69
|
+
browserContext: context,
|
|
70
|
+
url,
|
|
71
|
+
};
|
|
72
|
+
sessionManager.setPageRef(sessionId, url, pageRef);
|
|
73
|
+
// Attach auto-capture on navigation
|
|
74
|
+
setupAutoCapture(page, sessionId, sessionManager);
|
|
75
|
+
// Attach diagnostic collectors
|
|
76
|
+
const consoleCollector = attachConsoleCollector(page);
|
|
77
|
+
const errorCollector = attachErrorCollector(page);
|
|
78
|
+
const networkCollector = attachNetworkCollector(page);
|
|
79
|
+
sessionManager.setConsoleCollector(sessionId, url, consoleCollector);
|
|
80
|
+
sessionManager.setErrorCollector(sessionId, url, errorCollector);
|
|
81
|
+
sessionManager.setNetworkCollector(sessionId, url, networkCollector);
|
|
82
|
+
// Register browser cleanup as a session resource
|
|
83
|
+
sessionManager.addResource(sessionId, {
|
|
84
|
+
cleanup: async () => {
|
|
85
|
+
console.error(`[screenshot_web] Closing browser for ${url}`);
|
|
86
|
+
await context.close().catch(() => { });
|
|
87
|
+
await browser.close().catch(() => { });
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
console.error(`[screenshot_web] Browser ready for ${url}`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// Reuse existing page — navigate if URL changed
|
|
94
|
+
const currentUrl = pageRef.page.url();
|
|
95
|
+
if (currentUrl !== url) {
|
|
96
|
+
await pageRef.page.goto(url, {
|
|
97
|
+
waitUntil: "load",
|
|
98
|
+
timeout: 30000,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
console.error(`[screenshot_web] Capturing ${url} for session ${sessionId}`);
|
|
103
|
+
// Capture raw PNG
|
|
104
|
+
const rawBuffer = await capturePlaywrightPage(pageRef.page, {
|
|
105
|
+
fullPage: fullPage ?? false,
|
|
106
|
+
});
|
|
107
|
+
// Optimize: resize + WebP
|
|
108
|
+
const optimized = await optimizeScreenshot(rawBuffer, {
|
|
109
|
+
maxWidth: maxWidth ?? 1280,
|
|
110
|
+
quality: quality ?? 80,
|
|
111
|
+
});
|
|
112
|
+
const imageBase64 = optimized.data.toString("base64");
|
|
113
|
+
return createScreenshotResult({
|
|
114
|
+
sessionId,
|
|
115
|
+
type: "web",
|
|
116
|
+
url,
|
|
117
|
+
mode: fullPage ? "full-page" : "viewport",
|
|
118
|
+
width: optimized.width,
|
|
119
|
+
height: optimized.height,
|
|
120
|
+
originalSize: rawBuffer.length,
|
|
121
|
+
optimizedSize: optimized.data.length,
|
|
122
|
+
}, imageBase64, optimized.mimeType);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
126
|
+
return createToolError("Failed to capture web screenshot", message, "Check the URL is accessible. For dev servers, ensure the server is running first (use launch_web_server).");
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stop_process MCP tool (PROC-05)
|
|
3
|
+
* Stops all processes in a session by destroying the session and its resources
|
|
4
|
+
*/
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { SessionManager } from "../session-manager.js";
|
|
7
|
+
/**
|
|
8
|
+
* Register the stop_process tool with the MCP server
|
|
9
|
+
*
|
|
10
|
+
* Destroys a session, which triggers cleanup of all registered resources
|
|
11
|
+
* (process trees, Electron apps, etc.). This is intentionally session-wide
|
|
12
|
+
* because each session typically manages one application under test.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerStopProcessTool(server: McpServer, sessionManager: SessionManager): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stop_process MCP tool (PROC-05)
|
|
3
|
+
* Stops all processes in a session by destroying the session and its resources
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createToolError, createToolResult } from "../utils/errors.js";
|
|
7
|
+
/**
|
|
8
|
+
* Register the stop_process tool with the MCP server
|
|
9
|
+
*
|
|
10
|
+
* Destroys a session, which triggers cleanup of all registered resources
|
|
11
|
+
* (process trees, Electron apps, etc.). This is intentionally session-wide
|
|
12
|
+
* because each session typically manages one application under test.
|
|
13
|
+
*/
|
|
14
|
+
export function registerStopProcessTool(server, sessionManager) {
|
|
15
|
+
server.tool("stop_process", "Stop all processes in a session and clean up resources. Use to terminate running apps when done or to free ports.", {
|
|
16
|
+
sessionId: z.string().describe("Session ID whose processes to stop"),
|
|
17
|
+
}, async ({ sessionId }) => {
|
|
18
|
+
try {
|
|
19
|
+
// Validate session exists
|
|
20
|
+
const session = sessionManager.get(sessionId);
|
|
21
|
+
if (!session) {
|
|
22
|
+
const availableSessions = sessionManager.list();
|
|
23
|
+
return createToolError(`Session not found: ${sessionId}`, "The session may have already been ended or never existed", availableSessions.length > 0
|
|
24
|
+
? `Available sessions: ${availableSessions.join(", ")}`
|
|
25
|
+
: "No active sessions. Use list_sessions to check.");
|
|
26
|
+
}
|
|
27
|
+
console.error(`[stop_process] Stopping all processes in session ${sessionId}`);
|
|
28
|
+
// Destroy session -- triggers cleanup of all registered resources
|
|
29
|
+
await sessionManager.destroy(sessionId);
|
|
30
|
+
return createToolResult({
|
|
31
|
+
sessionId,
|
|
32
|
+
stopped: true,
|
|
33
|
+
message: "All processes in session stopped and resources cleaned up",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
return createToolError("Failed to stop processes", message, "The session may be in an inconsistent state. Try end_session or create a new session.");
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* type_text MCP tool
|
|
3
|
+
* Types text into input fields on web or Electron pages using Playwright Locator API
|
|
4
|
+
*/
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { SessionManager } from "../session-manager.js";
|
|
7
|
+
/**
|
|
8
|
+
* Register the type_text tool with the MCP server
|
|
9
|
+
*
|
|
10
|
+
* @param server - MCP server instance
|
|
11
|
+
* @param sessionManager - Session manager for resource tracking
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerTypeTextTool(server: McpServer, sessionManager: SessionManager): void;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* type_text MCP tool
|
|
3
|
+
* Types text into input fields on web or Electron pages using Playwright Locator API
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createToolError, createScreenshotResult } from "../utils/errors.js";
|
|
7
|
+
import { capturePlaywrightPage } from "../screenshot/capture.js";
|
|
8
|
+
import { optimizeScreenshot } from "../screenshot/optimize.js";
|
|
9
|
+
import { resolveSelector, getActivePage } from "../interaction/selectors.js";
|
|
10
|
+
/**
|
|
11
|
+
* Register the type_text tool with the MCP server
|
|
12
|
+
*
|
|
13
|
+
* @param server - MCP server instance
|
|
14
|
+
* @param sessionManager - Session manager for resource tracking
|
|
15
|
+
*/
|
|
16
|
+
export function registerTypeTextTool(server, sessionManager) {
|
|
17
|
+
server.tool("type_text", "Type text into an input field or textarea on a web or Electron page. Returns a screenshot after typing. Uses fill (paste) by default; set pressSequentially for apps with keystroke handlers.", {
|
|
18
|
+
sessionId: z
|
|
19
|
+
.string()
|
|
20
|
+
.describe("Session ID from create_session"),
|
|
21
|
+
selector: z
|
|
22
|
+
.string()
|
|
23
|
+
.describe("Element selector for the input field. CSS: #email, .search-box, input[name='query']. Role: role=textbox[name='Email']. Test ID: testid=search-input"),
|
|
24
|
+
text: z
|
|
25
|
+
.string()
|
|
26
|
+
.describe("Text to type into the field"),
|
|
27
|
+
pageIdentifier: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("URL or 'electron' to target a specific page. Omit if session has only one page."),
|
|
31
|
+
pressSequentially: z
|
|
32
|
+
.boolean()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Type one character at a time instead of fill/paste (default: false). Use for inputs with autocomplete or per-keystroke handlers."),
|
|
35
|
+
delay: z
|
|
36
|
+
.number()
|
|
37
|
+
.int()
|
|
38
|
+
.min(0)
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Delay between keystrokes in ms (only used with pressSequentially, default: 50)"),
|
|
41
|
+
clear: z
|
|
42
|
+
.boolean()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Clear the field before typing (default: true). Set false to append text."),
|
|
45
|
+
timeout: z
|
|
46
|
+
.number()
|
|
47
|
+
.int()
|
|
48
|
+
.min(0)
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("Max wait time in ms for element readiness (default: 30000)"),
|
|
51
|
+
}, async ({ sessionId, selector, text, pageIdentifier, pressSequentially, delay, clear, timeout, }) => {
|
|
52
|
+
try {
|
|
53
|
+
// Validate session exists
|
|
54
|
+
const session = sessionManager.get(sessionId);
|
|
55
|
+
if (!session) {
|
|
56
|
+
const availableSessions = sessionManager.list();
|
|
57
|
+
return createToolError(`Session not found: ${sessionId}`, "The session may have already been ended", availableSessions.length > 0
|
|
58
|
+
? `Available sessions: ${availableSessions.join(", ")}`
|
|
59
|
+
: "Create a session first with create_session.");
|
|
60
|
+
}
|
|
61
|
+
// Find the active page
|
|
62
|
+
const pageResult = getActivePage(sessionManager, sessionId, pageIdentifier);
|
|
63
|
+
if (!pageResult.success) {
|
|
64
|
+
return createToolError(pageResult.error, `Session: ${sessionId}`, pageResult.availablePages
|
|
65
|
+
? `Available pages: ${pageResult.availablePages.join(", ")}`
|
|
66
|
+
: undefined);
|
|
67
|
+
}
|
|
68
|
+
const { page } = pageResult;
|
|
69
|
+
// Resolve selector to Playwright Locator
|
|
70
|
+
const locator = resolveSelector(page, selector);
|
|
71
|
+
const effectiveTimeout = timeout ?? 30000;
|
|
72
|
+
// Perform the type action based on mode
|
|
73
|
+
if (clear === false && pressSequentially) {
|
|
74
|
+
// Append mode + pressSequentially: click to focus, then type char-by-char
|
|
75
|
+
await locator.click({ timeout: effectiveTimeout });
|
|
76
|
+
await locator.pressSequentially(text, {
|
|
77
|
+
delay: delay ?? 50,
|
|
78
|
+
timeout: effectiveTimeout,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else if (pressSequentially) {
|
|
82
|
+
// Clear first, then type char-by-char
|
|
83
|
+
await locator.fill("", { timeout: effectiveTimeout });
|
|
84
|
+
await locator.pressSequentially(text, {
|
|
85
|
+
delay: delay ?? 50,
|
|
86
|
+
timeout: effectiveTimeout,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else if (clear === false) {
|
|
90
|
+
// Append mode + fill: click to focus, then insertText to append
|
|
91
|
+
await locator.click({ timeout: effectiveTimeout });
|
|
92
|
+
await page.keyboard.insertText(text);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Default: fill() clears and types in one step
|
|
96
|
+
await locator.fill(text, { timeout: effectiveTimeout });
|
|
97
|
+
}
|
|
98
|
+
// Capture post-type screenshot
|
|
99
|
+
const rawBuffer = await capturePlaywrightPage(page, {
|
|
100
|
+
fullPage: false,
|
|
101
|
+
});
|
|
102
|
+
const optimized = await optimizeScreenshot(rawBuffer, {
|
|
103
|
+
maxWidth: 1280,
|
|
104
|
+
quality: 80,
|
|
105
|
+
});
|
|
106
|
+
const imageBase64 = optimized.data.toString("base64");
|
|
107
|
+
return createScreenshotResult({
|
|
108
|
+
sessionId,
|
|
109
|
+
action: "type",
|
|
110
|
+
selector,
|
|
111
|
+
textLength: text.length,
|
|
112
|
+
mode: pressSequentially ? "pressSequentially" : "fill",
|
|
113
|
+
success: true,
|
|
114
|
+
}, imageBase64, optimized.mimeType);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
118
|
+
// Strict mode violation: selector matched multiple elements
|
|
119
|
+
if (message.includes("strict mode violation")) {
|
|
120
|
+
return createToolError("Selector matched multiple elements", `Selector "${selector}" matched more than one element (strict mode violation)`, "Use a more specific selector or add :nth-child(), :first-of-type, or similar to target a single element.");
|
|
121
|
+
}
|
|
122
|
+
// Timeout: element not found or not actionable within timeout
|
|
123
|
+
if (message.includes("Timeout") ||
|
|
124
|
+
message.includes("timeout")) {
|
|
125
|
+
return createToolError("Element not found or not editable within timeout", `Selector "${selector}" did not match any visible, editable element within ${timeout ?? 30000}ms`, "Check the selector is correct, the element is visible and editable, or increase the timeout. Take a screenshot first to verify the page state.");
|
|
126
|
+
}
|
|
127
|
+
// Element is not editable (not an input/textarea/contenteditable)
|
|
128
|
+
if (message.includes("not an <input>") ||
|
|
129
|
+
message.includes("not editable") ||
|
|
130
|
+
message.includes("Element is not")) {
|
|
131
|
+
return createToolError("Element is not a text input", `Selector "${selector}" matched an element that cannot accept text input`, "Ensure the selector targets an <input>, <textarea>, or contenteditable element. Take a screenshot to verify.");
|
|
132
|
+
}
|
|
133
|
+
// Default error
|
|
134
|
+
return createToolError("Failed to type text", `Selector: "${selector}" — ${message}`, "Take a screenshot to verify the element exists and is an editable text field.");
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wait_for_element MCP tool
|
|
3
|
+
* Waits for an element to reach a specific state (visible, hidden, attached, detached)
|
|
4
|
+
* and returns a screenshot after the wait since the page likely changed.
|
|
5
|
+
*/
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { SessionManager } from "../session-manager.js";
|
|
8
|
+
/**
|
|
9
|
+
* Register the wait_for_element tool with the MCP server
|
|
10
|
+
*
|
|
11
|
+
* @param server - MCP server instance
|
|
12
|
+
* @param sessionManager - Session manager for resource tracking
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerWaitForElementTool(server: McpServer, sessionManager: SessionManager): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wait_for_element MCP tool
|
|
3
|
+
* Waits for an element to reach a specific state (visible, hidden, attached, detached)
|
|
4
|
+
* and returns a screenshot after the wait since the page likely changed.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { createToolError, createScreenshotResult } from "../utils/errors.js";
|
|
8
|
+
import { resolveSelector, getActivePage } from "../interaction/selectors.js";
|
|
9
|
+
import { capturePlaywrightPage } from "../screenshot/capture.js";
|
|
10
|
+
import { optimizeScreenshot } from "../screenshot/optimize.js";
|
|
11
|
+
/**
|
|
12
|
+
* Register the wait_for_element tool with the MCP server
|
|
13
|
+
*
|
|
14
|
+
* @param server - MCP server instance
|
|
15
|
+
* @param sessionManager - Session manager for resource tracking
|
|
16
|
+
*/
|
|
17
|
+
export function registerWaitForElementTool(server, sessionManager) {
|
|
18
|
+
server.tool("wait_for_element", "Wait for an element to reach a specific state. Use before interacting with elements that may not be ready yet (loading spinners, disabled buttons, lazy content).", {
|
|
19
|
+
sessionId: z
|
|
20
|
+
.string()
|
|
21
|
+
.describe("Session ID from create_session"),
|
|
22
|
+
selector: z
|
|
23
|
+
.string()
|
|
24
|
+
.describe("Element selector. CSS: #id, .class, div > span. Text: text=Click me. Role: role=button[name='Submit']. Test ID: testid=my-btn"),
|
|
25
|
+
state: z
|
|
26
|
+
.enum(["visible", "hidden", "attached", "detached"])
|
|
27
|
+
.describe("Target state to wait for. 'visible': element visible on page. 'hidden': element hidden or removed. 'attached': element in DOM (may be hidden). 'detached': element removed from DOM."),
|
|
28
|
+
pageIdentifier: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("URL or 'electron' to target a specific page. Omit if session has only one page."),
|
|
32
|
+
timeout: z
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.min(0)
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Max wait time in ms (default: 30000)"),
|
|
38
|
+
}, async ({ sessionId, selector, state, pageIdentifier, timeout }) => {
|
|
39
|
+
try {
|
|
40
|
+
// Validate session exists
|
|
41
|
+
const session = sessionManager.get(sessionId);
|
|
42
|
+
if (!session) {
|
|
43
|
+
const availableSessions = sessionManager.list();
|
|
44
|
+
return createToolError(`Session not found: ${sessionId}`, "The session may have already been ended", availableSessions.length > 0
|
|
45
|
+
? `Available sessions: ${availableSessions.join(", ")}`
|
|
46
|
+
: "Create a session first with create_session.");
|
|
47
|
+
}
|
|
48
|
+
// Find the active page
|
|
49
|
+
const pageResult = getActivePage(sessionManager, sessionId, pageIdentifier);
|
|
50
|
+
if (!pageResult.success) {
|
|
51
|
+
return createToolError(pageResult.error, `Session: ${sessionId}`, pageResult.availablePages
|
|
52
|
+
? `Available pages: ${pageResult.availablePages.join(", ")}`
|
|
53
|
+
: undefined);
|
|
54
|
+
}
|
|
55
|
+
const { page } = pageResult;
|
|
56
|
+
const effectiveTimeout = timeout ?? 30000;
|
|
57
|
+
// Resolve selector to Playwright Locator
|
|
58
|
+
const locator = resolveSelector(page, selector);
|
|
59
|
+
// Wait for the specified state
|
|
60
|
+
await locator.waitFor({ state, timeout: effectiveTimeout });
|
|
61
|
+
// Capture post-wait screenshot (page likely changed during wait)
|
|
62
|
+
const rawBuffer = await capturePlaywrightPage(page, {
|
|
63
|
+
fullPage: false,
|
|
64
|
+
});
|
|
65
|
+
const optimized = await optimizeScreenshot(rawBuffer, {
|
|
66
|
+
maxWidth: 1280,
|
|
67
|
+
quality: 80,
|
|
68
|
+
});
|
|
69
|
+
const imageBase64 = optimized.data.toString("base64");
|
|
70
|
+
return createScreenshotResult({
|
|
71
|
+
sessionId,
|
|
72
|
+
action: "wait_for_element",
|
|
73
|
+
selector,
|
|
74
|
+
state,
|
|
75
|
+
success: true,
|
|
76
|
+
}, imageBase64, optimized.mimeType);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
// Strict mode violation: selector matched multiple elements
|
|
81
|
+
if (message.includes("strict mode violation")) {
|
|
82
|
+
return createToolError("Selector matched multiple elements", `Selector "${selector}" matched more than one element (strict mode violation)`, "Use a more specific selector or add :nth-child(), :first-of-type, or similar to target a single element.");
|
|
83
|
+
}
|
|
84
|
+
// Timeout: element did not reach expected state
|
|
85
|
+
if (message.includes("Timeout") ||
|
|
86
|
+
message.includes("timeout")) {
|
|
87
|
+
return createToolError(`Element did not reach state '${state}' within ${timeout ?? 30000}ms`, `Selector: "${selector}", target state: "${state}"`, `The element exists but did not become ${state}. Take a screenshot to see the current page state.`);
|
|
88
|
+
}
|
|
89
|
+
// Default error
|
|
90
|
+
return createToolError("Failed to wait for element", `Selector: "${selector}", state: "${state}" — ${message}`, "Take a screenshot to verify the element exists and is visible on the page.");
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type definitions for the Feedback MCP server
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Resource with cleanup capability
|
|
6
|
+
*/
|
|
7
|
+
export interface Resource {
|
|
8
|
+
cleanup: () => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Session tracking active resources
|
|
12
|
+
*/
|
|
13
|
+
export interface Session {
|
|
14
|
+
id: string;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
resources: Resource[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* MCP tool result format (supports text and image content)
|
|
20
|
+
*/
|
|
21
|
+
export type ToolResult = {
|
|
22
|
+
content: Array<{
|
|
23
|
+
type: "text";
|
|
24
|
+
text: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: "image";
|
|
27
|
+
data: string;
|
|
28
|
+
mimeType: string;
|
|
29
|
+
}>;
|
|
30
|
+
isError?: boolean;
|
|
31
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured error formatting for tool results
|
|
3
|
+
*/
|
|
4
|
+
import { ToolResult } from "../types/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* Create a tool error response with structured information
|
|
7
|
+
* @param message Error message
|
|
8
|
+
* @param context Optional context about what was being attempted
|
|
9
|
+
* @param suggestedFix Optional suggestion for how to fix the issue
|
|
10
|
+
* @returns ToolResult with isError: true
|
|
11
|
+
*/
|
|
12
|
+
export declare function createToolError(message: string, context?: string, suggestedFix?: string): ToolResult;
|
|
13
|
+
/**
|
|
14
|
+
* Create a successful tool result
|
|
15
|
+
* @param data Data to serialize as JSON
|
|
16
|
+
* @returns ToolResult with isError: false
|
|
17
|
+
*/
|
|
18
|
+
export declare function createToolResult(data: unknown): ToolResult;
|
|
19
|
+
/**
|
|
20
|
+
* Create a screenshot tool result with text metadata and image content
|
|
21
|
+
* @param metadata Metadata to include as JSON text
|
|
22
|
+
* @param imageBase64 Base64-encoded image data
|
|
23
|
+
* @param mimeType Image MIME type (default: 'image/webp')
|
|
24
|
+
* @returns ToolResult with text + image content
|
|
25
|
+
*/
|
|
26
|
+
export declare function createScreenshotResult(metadata: Record<string, unknown>, imageBase64: string, mimeType?: string): ToolResult;
|