mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-04-01 18:06:46 +00:00
mcp/cdp: fix inactivity timeout
- Fixed CDP inactivity timeout by resetting it when the browser is busy (loading or executing macrotasks). - Removed the placeholder screenshot tool. - Refactored MCP tool schemas to constants to avoid duplication.
This commit is contained in:
@@ -330,6 +330,11 @@ pub const Client = struct {
|
||||
ms_remaining = self.ws.timeout_ms;
|
||||
},
|
||||
.done => {
|
||||
if (self.isBusy()) {
|
||||
last_message = milliTimestamp(.monotonic);
|
||||
ms_remaining = self.ws.timeout_ms;
|
||||
continue;
|
||||
}
|
||||
const now = milliTimestamp(.monotonic);
|
||||
const elapsed = now - last_message;
|
||||
if (elapsed >= ms_remaining) {
|
||||
@@ -343,6 +348,24 @@ pub const Client = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn isBusy(self: *const Client) bool {
|
||||
if (self.http.active > 0 or self.http.intercepted > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cdp = switch (self.mode) {
|
||||
.cdp => |*c| c,
|
||||
.http => return false,
|
||||
};
|
||||
|
||||
const session = cdp.browser.session orelse return false;
|
||||
if (session.browser.hasBackgroundTasks() or session.browser.msToNextMacrotask() != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn blockingReadStart(ctx: *anyopaque) bool {
|
||||
const self: *Client = @ptrCast(@alignCast(ctx));
|
||||
self.ws.setBlocking(true) catch |err| {
|
||||
|
||||
@@ -9,89 +9,72 @@ const protocol = @import("protocol.zig");
|
||||
const Server = @import("Server.zig");
|
||||
const CDPNode = @import("../cdp/Node.zig");
|
||||
|
||||
const screenshot_png = @embedFile("../cdp/domains/screenshot.png");
|
||||
const goto_schema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "url": { "type": "string", "description": "The URL to navigate to, must be a valid URL." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." }
|
||||
\\ },
|
||||
\\ "required": ["url"]
|
||||
\\}
|
||||
);
|
||||
|
||||
fn base64Encode(arena: std.mem.Allocator, input: []const u8) ![]const u8 {
|
||||
const encoder = std.base64.standard.Encoder;
|
||||
const buf = try arena.alloc(u8, encoder.calcSize(input.len));
|
||||
_ = encoder.encode(buf, input);
|
||||
return buf;
|
||||
}
|
||||
const url_params_schema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before processing." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." }
|
||||
\\ }
|
||||
\\}
|
||||
);
|
||||
|
||||
const evaluate_schema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "script": { "type": "string" },
|
||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before evaluating." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." }
|
||||
\\ },
|
||||
\\ "required": ["script"]
|
||||
\\}
|
||||
);
|
||||
|
||||
pub const tool_list = [_]protocol.Tool{
|
||||
.{
|
||||
.name = "goto",
|
||||
.description = "Navigate to a specified URL and load the page in memory so it can be reused later for info extraction.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "url": { "type": "string", "description": "The URL to navigate to, must be a valid URL." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." }
|
||||
\\ },
|
||||
\\ "required": ["url"]
|
||||
\\}
|
||||
),
|
||||
.inputSchema = goto_schema,
|
||||
},
|
||||
.{
|
||||
.name = "navigate",
|
||||
.description = "Alias for goto. Navigate to a specified URL and load the page in memory.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "url": { "type": "string", "description": "The URL to navigate to, must be a valid URL." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." }
|
||||
\\ },
|
||||
\\ "required": ["url"]
|
||||
\\}
|
||||
),
|
||||
.inputSchema = goto_schema,
|
||||
},
|
||||
.{
|
||||
.name = "markdown",
|
||||
.description = "Get the page content in markdown format. If a url is provided, it navigates to that url first.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before fetching markdown." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." }
|
||||
\\ }
|
||||
\\}
|
||||
),
|
||||
.inputSchema = url_params_schema,
|
||||
},
|
||||
.{
|
||||
.name = "links",
|
||||
.description = "Extract all links in the opened page. If a url is provided, it navigates to that url first.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before extracting links." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." }
|
||||
\\ }
|
||||
\\}
|
||||
),
|
||||
.inputSchema = url_params_schema,
|
||||
},
|
||||
.{
|
||||
.name = "evaluate",
|
||||
.description = "Evaluate JavaScript in the current page context. If a url is provided, it navigates to that url first.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "script": { "type": "string" },
|
||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before evaluating." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." }
|
||||
\\ },
|
||||
\\ "required": ["script"]
|
||||
\\}
|
||||
),
|
||||
.inputSchema = evaluate_schema,
|
||||
},
|
||||
.{
|
||||
.name = "eval",
|
||||
.description = "Alias for evaluate. Evaluate JavaScript in the current page context.",
|
||||
.inputSchema = evaluate_schema,
|
||||
},
|
||||
.{
|
||||
.name = "semantic_tree",
|
||||
@@ -125,44 +108,17 @@ pub const tool_list = [_]protocol.Tool{
|
||||
.{
|
||||
.name = "interactiveElements",
|
||||
.description = "Extract interactive elements from the opened page. If a url is provided, it navigates to that url first.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before extracting interactive elements." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." }
|
||||
\\ }
|
||||
\\}
|
||||
),
|
||||
.inputSchema = url_params_schema,
|
||||
},
|
||||
.{
|
||||
.name = "structuredData",
|
||||
.description = "Extract structured data (like JSON-LD, OpenGraph, etc) from the opened page. If a url is provided, it navigates to that url first.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before extracting structured data." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." }
|
||||
\\ }
|
||||
\\}
|
||||
),
|
||||
.inputSchema = url_params_schema,
|
||||
},
|
||||
.{
|
||||
.name = "detectForms",
|
||||
.description = "Detect all forms on the page and return their structure including fields, types, and required status. If a url is provided, it navigates to that url first.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before detecting forms." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." }
|
||||
\\ }
|
||||
\\}
|
||||
),
|
||||
.inputSchema = url_params_schema,
|
||||
},
|
||||
.{
|
||||
.name = "click",
|
||||
@@ -219,36 +175,6 @@ pub const tool_list = [_]protocol.Tool{
|
||||
\\}
|
||||
),
|
||||
},
|
||||
.{
|
||||
.name = "eval",
|
||||
.description = "Alias for evaluate. Evaluate JavaScript in the current page context.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "script": { "type": "string" },
|
||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before evaluating." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." }
|
||||
\\ },
|
||||
\\ "required": ["script"]
|
||||
\\}
|
||||
),
|
||||
},
|
||||
.{
|
||||
.name = "screenshot",
|
||||
.description = "Capture a screenshot of the current page. Returns the screenshot as a base64 encoded PNG.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before taking the screenshot." },
|
||||
\\ "timeout": { "type": "integer", "description": "Optional timeout in milliseconds. Defaults to 10000." },
|
||||
\\ "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle", "done"], "description": "Optional wait strategy. Defaults to 'done'." }
|
||||
\\ }
|
||||
\\}
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
pub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
|
||||
@@ -356,7 +282,6 @@ const ToolAction = enum {
|
||||
fill,
|
||||
scroll,
|
||||
waitForSelector,
|
||||
screenshot,
|
||||
};
|
||||
|
||||
const tool_map = std.StaticStringMap(ToolAction).initComptime(.{
|
||||
@@ -375,7 +300,6 @@ const tool_map = std.StaticStringMap(ToolAction).initComptime(.{
|
||||
.{ "fill", .fill },
|
||||
.{ "scroll", .scroll },
|
||||
.{ "waitForSelector", .waitForSelector },
|
||||
.{ "screenshot", .screenshot },
|
||||
});
|
||||
|
||||
pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
|
||||
@@ -410,7 +334,6 @@ pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Reque
|
||||
.fill => try handleFill(server, arena, req.id.?, call_params.arguments),
|
||||
.scroll => try handleScroll(server, arena, req.id.?, call_params.arguments),
|
||||
.waitForSelector => try handleWaitForSelector(server, arena, req.id.?, call_params.arguments),
|
||||
.screenshot => try handleScreenshot(server, arena, req.id.?, call_params.arguments),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,19 +636,6 @@ fn handleWaitForSelector(server: *Server, arena: std.mem.Allocator, id: std.json
|
||||
return server.sendResult(id, protocol.CallToolResult(protocol.TextContent([]const u8)){ .content = &content });
|
||||
}
|
||||
|
||||
fn handleScreenshot(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
|
||||
const args = try parseArgsOrDefault(UrlParams, arena, arguments, server, id);
|
||||
_ = try ensurePage(server, id, args.url, args.timeout, args.waitUntil);
|
||||
|
||||
const b64 = try base64Encode(arena, screenshot_png);
|
||||
|
||||
const content = [_]protocol.ImageContent([]const u8){.{
|
||||
.data = b64,
|
||||
.mimeType = "image/png",
|
||||
}};
|
||||
try server.sendResult(id, protocol.CallToolResult(protocol.ImageContent([]const u8)){ .content = &content });
|
||||
}
|
||||
|
||||
fn ensurePage(server: *Server, id: std.json.Value, url: ?[:0]const u8, timeout: ?u32, waitUntil: ?lp.Config.WaitUntil) !*lp.Page {
|
||||
if (url) |u| {
|
||||
try performGoto(server, u, id, timeout, waitUntil);
|
||||
|
||||
Reference in New Issue
Block a user