mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 12:44:43 +00:00
mcp: improve event loop and response handling
- Use an allocating writer in `sendResponse` to handle large payloads. - Update the main loop to tick the HTTP client and cap poll timeouts. - Update protocol version and minify tool input schemas.
This commit is contained in:
@@ -12,7 +12,7 @@ app: *App,
|
|||||||
|
|
||||||
http_client: *HttpClient,
|
http_client: *HttpClient,
|
||||||
notification: *lp.Notification,
|
notification: *lp.Notification,
|
||||||
browser: lp.Browser,
|
browser: *lp.Browser,
|
||||||
session: *lp.Session,
|
session: *lp.Session,
|
||||||
page: *lp.Page,
|
page: *lp.Page,
|
||||||
|
|
||||||
@@ -53,12 +53,12 @@ pub fn deinit(self: *Self) void {
|
|||||||
self.allocator.destroy(self);
|
self.allocator.destroy(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sendResponse(_: *Self, response: anytype) !void {
|
pub fn sendResponse(self: *Self, response: anytype) !void {
|
||||||
var buffer: [8192]u8 = undefined;
|
var aw: std.Io.Writer.Allocating = .init(self.allocator);
|
||||||
var stdout = std.fs.File.stdout().writer(&buffer);
|
defer aw.deinit();
|
||||||
try std.json.Stringify.value(response, .{ .emit_null_optional_fields = false }, &stdout.interface);
|
try std.json.Stringify.value(response, .{ .emit_null_optional_fields = false }, &aw.writer);
|
||||||
try stdout.interface.writeByte('\n');
|
try aw.writer.writeByte('\n');
|
||||||
try stdout.interface.flush();
|
try std.fs.File.stdout().writeAll(aw.written());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sendResult(self: *Self, id: std.json.Value, result: anytype) !void {
|
pub fn sendResult(self: *Self, id: std.json.Value, result: anytype) !void {
|
||||||
|
|||||||
@@ -17,24 +17,36 @@ pub fn processRequests(server: *Server) !void {
|
|||||||
|
|
||||||
const reader = poller.reader(.stdin);
|
const reader = poller.reader(.stdin);
|
||||||
|
|
||||||
var arena: std.heap.ArenaAllocator = .init(server.allocator);
|
var arena_instance = std.heap.ArenaAllocator.init(server.allocator);
|
||||||
defer arena.deinit();
|
defer arena_instance.deinit();
|
||||||
|
const arena = arena_instance.allocator();
|
||||||
|
|
||||||
while (server.is_running.load(.acquire)) {
|
while (server.is_running.load(.acquire)) {
|
||||||
|
// Run ready browser tasks and get time to next one
|
||||||
const ms_to_next_task = (try server.browser.runMacrotasks()) orelse 10_000;
|
const ms_to_next_task = (try server.browser.runMacrotasks()) orelse 10_000;
|
||||||
|
|
||||||
// Poll until the next macrotask is scheduled. This will block if no data is available.
|
// Keep the loop responsive to network events and stdin.
|
||||||
const poll_ok = try poller.pollTimeout(ms_to_next_task * std.time.ns_per_ms);
|
const ms_to_wait: u64 = @min(50, ms_to_next_task);
|
||||||
|
|
||||||
|
// Wait for stdin activity for up to ms_to_wait.
|
||||||
|
const poll_result = try poller.pollTimeout(ms_to_wait * @as(u64, std.time.ns_per_ms));
|
||||||
|
|
||||||
|
// Process any pending network I/O
|
||||||
|
_ = try server.http_client.tick(0);
|
||||||
|
|
||||||
|
// Run V8 microtasks and internal message loop
|
||||||
|
server.browser.runMessageLoop();
|
||||||
|
|
||||||
|
// Process all complete lines available in the buffer
|
||||||
while (true) {
|
while (true) {
|
||||||
const buffered = reader.buffered();
|
const buffered = reader.buffered();
|
||||||
if (std.mem.indexOfScalar(u8, buffered, '\n')) |idx| {
|
if (std.mem.indexOfScalar(u8, buffered, '\n')) |idx| {
|
||||||
const line = buffered[0..idx];
|
const line = buffered[0..idx];
|
||||||
if (line.len > 0) {
|
if (line.len > 0) {
|
||||||
handleMessage(server, arena.allocator(), line) catch |err| {
|
handleMessage(server, arena, line) catch |err| {
|
||||||
log.warn(.mcp, "Error processing message", .{ .err = err });
|
log.warn(.mcp, "Error processing message", .{ .err = err });
|
||||||
};
|
};
|
||||||
_ = arena.reset(.{ .retain_with_limit = 32 * 1024 });
|
_ = arena_instance.reset(.{ .retain_with_limit = 32 * 1024 });
|
||||||
}
|
}
|
||||||
reader.toss(idx + 1);
|
reader.toss(idx + 1);
|
||||||
} else {
|
} else {
|
||||||
@@ -42,18 +54,14 @@ pub fn processRequests(server: *Server) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!poll_ok) {
|
// pollTimeout returns false when all streams are closed (EOF on stdin)
|
||||||
// Check if we have any data left in the buffer that didn't end with a newline
|
if (!poll_result) {
|
||||||
const buffered = reader.buffered();
|
const buffered = reader.buffered();
|
||||||
if (buffered.len > 0) {
|
if (buffered.len > 0) {
|
||||||
handleMessage(server, arena.allocator(), buffered) catch |err| {
|
handleMessage(server, arena, buffered) catch {};
|
||||||
log.warn(.mcp, "Error processing last message", .{ .err = err });
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
server.browser.runMessageLoop();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +98,10 @@ fn handleMessage(server: *Server, arena: std.mem.Allocator, msg: []const u8) !vo
|
|||||||
|
|
||||||
fn handleInitialize(server: *Server, req: protocol.Request) !void {
|
fn handleInitialize(server: *Server, req: protocol.Request) !void {
|
||||||
const result = protocol.InitializeResult{
|
const result = protocol.InitializeResult{
|
||||||
.protocolVersion = "2024-11-05",
|
.protocolVersion = "2025-11-25",
|
||||||
.capabilities = .{
|
.capabilities = .{
|
||||||
.logging = .{},
|
.logging = .{},
|
||||||
|
.prompts = .{ .listChanged = false },
|
||||||
.resources = .{ .subscribe = false, .listChanged = false },
|
.resources = .{ .subscribe = false, .listChanged = false },
|
||||||
.tools = .{ .listChanged = false },
|
.tools = .{ .listChanged = false },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,79 +13,32 @@ pub const tool_list = [_]protocol.Tool{
|
|||||||
.{
|
.{
|
||||||
.name = "goto",
|
.name = "goto",
|
||||||
.description = "Navigate to a specified URL and load the page in memory so it can be reused later for info extraction.",
|
.description = "Navigate to a specified URL and load the page in memory so it can be reused later for info extraction.",
|
||||||
.inputSchema = .{ .json =
|
.inputSchema = .{ .json = "{\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\",\"description\":\"The URL to navigate to, must be a valid URL.\"}},\"required\":[\"url\"]}" },
|
||||||
\\{
|
|
||||||
\\ "type": "object",
|
|
||||||
\\ "properties": {
|
|
||||||
\\ "url": { "type": "string", "description": "The URL to navigate to, must be a valid URL." }
|
|
||||||
\\ },
|
|
||||||
\\ "required": ["url"]
|
|
||||||
\\}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.name = "search",
|
.name = "search",
|
||||||
.description = "Use a search engine to look for specific words, terms, sentences. The search page will then be loaded in memory.",
|
.description = "Use a search engine to look for specific words, terms, sentences. The search page will then be loaded in memory.",
|
||||||
.inputSchema = .{ .json =
|
.inputSchema = .{ .json = "{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\",\"description\":\"The text to search for, must be a valid search query.\"}},\"required\":[\"text\"]}" },
|
||||||
\\{
|
|
||||||
\\ "type": "object",
|
|
||||||
\\ "properties": {
|
|
||||||
\\ "text": { "type": "string", "description": "The text to search for, must be a valid search query." }
|
|
||||||
\\ },
|
|
||||||
\\ "required": ["text"]
|
|
||||||
\\}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.name = "markdown",
|
.name = "markdown",
|
||||||
.description = "Get the page content in markdown format. If a url is provided, it navigates to that url first.",
|
.description = "Get the page content in markdown format. If a url is provided, it navigates to that url first.",
|
||||||
.inputSchema = .{ .json =
|
.inputSchema = .{ .json = "{\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\",\"description\":\"Optional URL to navigate to before fetching markdown.\"}}}" },
|
||||||
\\{
|
|
||||||
\\ "type": "object",
|
|
||||||
\\ "properties": {
|
|
||||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before fetching markdown." }
|
|
||||||
\\ }
|
|
||||||
\\}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.name = "links",
|
.name = "links",
|
||||||
.description = "Extract all links in the opened page. If a url is provided, it navigates to that url first.",
|
.description = "Extract all links in the opened page. If a url is provided, it navigates to that url first.",
|
||||||
.inputSchema = .{ .json =
|
.inputSchema = .{ .json = "{\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\",\"description\":\"Optional URL to navigate to before extracting links.\"}}}" },
|
||||||
\\{
|
|
||||||
\\ "type": "object",
|
|
||||||
\\ "properties": {
|
|
||||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before extracting links." }
|
|
||||||
\\ }
|
|
||||||
\\}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.name = "evaluate",
|
.name = "evaluate",
|
||||||
.description = "Evaluate JavaScript in the current page context. If a url is provided, it navigates to that url first.",
|
.description = "Evaluate JavaScript in the current page context. If a url is provided, it navigates to that url first.",
|
||||||
.inputSchema = .{ .json =
|
.inputSchema = .{ .json = "{\"type\":\"object\",\"properties\":{\"script\":{\"type\":\"string\"},\"url\":{\"type\":\"string\",\"description\":\"Optional URL to navigate to before evaluating.\"}},\"required\":[\"script\"]}" },
|
||||||
\\{
|
|
||||||
\\ "type": "object",
|
|
||||||
\\ "properties": {
|
|
||||||
\\ "script": { "type": "string" },
|
|
||||||
\\ "url": { "type": "string", "description": "Optional URL to navigate to before evaluating." }
|
|
||||||
\\ },
|
|
||||||
\\ "required": ["script"]
|
|
||||||
\\}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
.name = "over",
|
.name = "over",
|
||||||
.description = "Used to indicate that the task is over and give the final answer if there is any. This is the last tool to be called in a task.",
|
.description = "Used to indicate that the task is over and give the final answer if there is any. This is the last tool to be called in a task.",
|
||||||
.inputSchema = .{ .json =
|
.inputSchema = .{ .json = "{\"type\":\"object\",\"properties\":{\"result\":{\"type\":\"string\",\"description\":\"The final result of the task.\"}},\"required\":[\"result\"]}" },
|
||||||
\\{
|
|
||||||
\\ "type": "object",
|
|
||||||
\\ "properties": {
|
|
||||||
\\ "result": { "type": "string", "description": "The final result of the task." }
|
|
||||||
\\ },
|
|
||||||
\\ "required": ["result"]
|
|
||||||
\\}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user