diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 9071e07c..4ddb2da1 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -736,6 +736,7 @@ fn attemptSelfHeal(self: *Self, arena: std.mem.Allocator, failed_command: []cons return cmds; } self.messages.shrinkRetainingCapacity(msg_baseline); + break; } return null; } diff --git a/src/agent/Terminal.zig b/src/agent/Terminal.zig index dc1c1580..716945ce 100644 --- a/src/agent/Terminal.zig +++ b/src/agent/Terminal.zig @@ -525,8 +525,7 @@ pub fn printErrorFmt(_: *Self, comptime fmt: []const u8, args: anytype) void { } pub fn printInfo(self: *Self, msg: []const u8) void { - if (!self.isRepl() and !atLeast(self.verbosity, .medium)) return; - std.debug.print("{s}{s}{s}\n", .{ ansi.dim, msg, ansi.reset }); + self.printInfoFmt("{s}", .{msg}); } pub fn printInfoFmt(self: *Self, comptime fmt: []const u8, args: anytype) void { diff --git a/src/mcp/Server.zig b/src/mcp/Server.zig index b955401b..707cb1e9 100644 --- a/src/mcp/Server.zig +++ b/src/mcp/Server.zig @@ -77,9 +77,17 @@ pub fn deinit(self: *Self) void { self.allocator.destroy(self); } +pub fn sendError(self: *Self, id: std.json.Value, code: protocol.ErrorCode, message: []const u8) !void { + return self.transport.sendError(id, code, message); +} + +pub fn sendResult(self: *Self, id: std.json.Value, result: anytype) !void { + return self.transport.sendResult(id, result); +} + pub fn handleInitialize(self: *Self, req: protocol.Request) !void { const id = req.id orelse return; - try self.transport.sendResult(id, protocol.InitializeResult{ + try self.sendResult(id, protocol.InitializeResult{ .protocolVersion = @tagName(protocol.Version.default), .capabilities = .{ .resources = .{}, diff --git a/src/mcp/resources.zig b/src/mcp/resources.zig index 1170e2ff..ed413c33 100644 --- a/src/mcp/resources.zig +++ b/src/mcp/resources.zig @@ -23,7 +23,7 @@ pub const resource_list = [_]protocol.Resource{ pub fn handleList(server: *Server, req: protocol.Request) !void { const id = req.id orelse return; - try server.transport.sendResult(id, .{ .resources = &resource_list }); + try server.sendResult(id, .{ .resources = &resource_list }); } const ReadParams = struct { @@ -74,20 +74,20 @@ const resource_map = std.StaticStringMap(ResourceUri).initComptime(.{ pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void { if (req.params == null or req.id == null) { - return server.transport.sendError(req.id orelse .{ .integer = -1 }, .InvalidParams, "Missing params"); + return server.sendError(req.id orelse .{ .integer = -1 }, .InvalidParams, "Missing params"); } const req_id = req.id.?; const params = std.json.parseFromValueLeaky(ReadParams, arena, req.params.?, .{ .ignore_unknown_fields = true }) catch { - return server.transport.sendError(req_id, .InvalidParams, "Invalid params"); + return server.sendError(req_id, .InvalidParams, "Invalid params"); }; const uri = resource_map.get(params.uri) orelse { - return server.transport.sendError(req_id, .InvalidRequest, "Resource not found"); + return server.sendError(req_id, .InvalidRequest, "Resource not found"); }; const frame = server.session.currentFrame() orelse { - return server.transport.sendError(req_id, .FrameNotLoaded, "Page not loaded"); + return server.sendError(req_id, .FrameNotLoaded, "Page not loaded"); }; const format: Format = switch (uri) { @@ -106,7 +106,7 @@ pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Reque .text = .{ .frame = frame, .format = format }, }}, }; - server.transport.sendResult(req_id, result) catch { - return server.transport.sendError(req_id, .InternalError, "Failed to serialize resource content"); + server.sendResult(req_id, result) catch { + return server.sendError(req_id, .InternalError, "Failed to serialize resource content"); }; } diff --git a/src/mcp/router.zig b/src/mcp/router.zig index bb90058d..3572428d 100644 --- a/src/mcp/router.zig +++ b/src/mcp/router.zig @@ -22,7 +22,7 @@ pub fn processRequests(server: anytype, reader: *std.io.Reader) !void { const buffered_line = reader.takeDelimiter('\n') catch |err| switch (err) { error.StreamTooLong => { log.err(.mcp, "Message too long", .{}); - try server.transport.sendError(.null, .InvalidRequest, "Message too long"); + try server.sendError(.null, .InvalidRequest, "Message too long"); continue; }, else => return err, @@ -62,13 +62,13 @@ pub fn handleMessage(server: anytype, arena: std.mem.Allocator, msg: []const u8) .ignore_unknown_fields = true, }) catch |err| { log.warn(.mcp, "JSON Parse Error", .{ .err = err, .msg = msg }); - try server.transport.sendError(.null, .ParseError, "Parse error"); + try server.sendError(.null, .ParseError, "Parse error"); return; }; const method = method_map.get(req.method) orelse { if (req.id != null) { - try server.transport.sendError(req.id.?, .MethodNotFound, "Method not found"); + try server.sendError(req.id.?, .MethodNotFound, "Method not found"); } return; }; @@ -88,13 +88,13 @@ fn handleOptional(server: anytype, req: protocol.Request, comptime method: []con if (@hasDecl(@TypeOf(server.*), method)) { try @call(.auto, @field(@TypeOf(server.*), method), .{server} ++ args); } else if (req.id) |id| { - try server.transport.sendError(id, .MethodNotFound, "Method not supported"); + try server.sendError(id, .MethodNotFound, "Method not supported"); } } fn handlePing(server: anytype, req: protocol.Request) !void { const id = req.id orelse return; - try server.transport.sendResult(id, .{}); + try server.sendResult(id, .{}); } const Server = @import("Server.zig"); diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index 36f912d2..79a32bbd 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -112,25 +112,38 @@ const extra_tools = [_]protocol.Tool{ const all_tools = browser_tool_list ++ extra_tools; +/// Tools that bypass the browser-tool dispatch and have their own handlers. +const ExtraTool = enum { + record_start, + record_stop, + record_comment, + script_step, + script_heal, +}; + pub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void { _ = arena; const id = req.id orelse return; - try server.transport.sendResult(id, .{ .tools = &all_tools }); + try server.sendResult(id, .{ .tools = &all_tools }); } pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void { const id = req.id orelse return; - const params = req.params orelse return server.transport.sendError(id, .InvalidParams, "Missing params"); + const params = req.params orelse return server.sendError(id, .InvalidParams, "Missing params"); const call_params = browser_tools.parseValue(protocol.CallParams, arena, params) catch { - return server.transport.sendError(id, .InvalidParams, "Invalid params"); + return server.sendError(id, .InvalidParams, "Invalid params"); }; - if (std.mem.eql(u8, call_params.name, "record_start")) return handleRecordStart(server, arena, id, call_params.arguments); - if (std.mem.eql(u8, call_params.name, "record_stop")) return handleRecordStop(server, arena, id); - if (std.mem.eql(u8, call_params.name, "record_comment")) return handleRecordComment(server, arena, id, call_params.arguments); - if (std.mem.eql(u8, call_params.name, "script_step")) return handleScriptStep(server, arena, id, call_params.arguments); - if (std.mem.eql(u8, call_params.name, "script_heal")) return handleScriptHeal(server, arena, id, call_params.arguments); + if (std.meta.stringToEnum(ExtraTool, call_params.name)) |tool| { + return switch (tool) { + .record_start => handleRecordStart(server, arena, id, call_params.arguments), + .record_stop => handleRecordStop(server, arena, id), + .record_comment => handleRecordComment(server, arena, id, call_params.arguments), + .script_step => handleScriptStep(server, arena, id, call_params.arguments), + .script_heal => handleScriptHeal(server, arena, id, call_params.arguments), + }; + } return dispatchBrowserTool(server, arena, id, call_params.name, call_params.arguments); } @@ -146,7 +159,7 @@ fn dispatchBrowserTool( arguments: ?std.json.Value, ) !void { const action = std.meta.stringToEnum(browser_tools.Action, name) orelse { - return server.transport.sendError(id, .MethodNotFound, "Tool not found"); + return server.sendError(id, .MethodNotFound, "Tool not found"); }; // JS errors are returned as isError tool results, not protocol errors @@ -162,7 +175,7 @@ fn dispatchBrowserTool( error.NodeNotFound, error.InvalidParams => .InvalidParams, error.NavigationFailed, error.InternalError, error.OutOfMemory => .InternalError, }; - return server.transport.sendError(id, code, @errorName(err)); + return server.sendError(id, code, @errorName(err)); }; recordIfActive(server, name, arguments); @@ -186,7 +199,7 @@ fn handleRecordStart(server: *Server, arena: std.mem.Allocator, id: std.json.Val } const Args = struct { path: []const u8 }; const args = browser_tools.parseArgs(Args, arena, arguments) catch { - return server.transport.sendError(id, .InvalidParams, "expected { path: string }"); + return server.sendError(id, .InvalidParams, "expected { path: string }"); }; if (!script.isPathSafe(args.path)) { @@ -228,7 +241,7 @@ fn handleRecordComment(server: *Server, arena: std.mem.Allocator, id: std.json.V } const Args = struct { text: []const u8 }; const args = browser_tools.parseArgs(Args, arena, arguments) catch { - return server.transport.sendError(id, .InvalidParams, "expected { text: string }"); + return server.sendError(id, .InvalidParams, "expected { text: string }"); }; server.recorder.?.recordComment(args.text); @@ -239,7 +252,7 @@ fn handleRecordComment(server: *Server, arena: std.mem.Allocator, id: std.json.V fn handleScriptStep(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { const Args = struct { line: []const u8 }; const args = browser_tools.parseArgs(Args, arena, arguments) catch { - return server.transport.sendError(id, .InvalidParams, "expected { line: string }"); + return server.sendError(id, .InvalidParams, "expected { line: string }"); }; const cmd = Command.parse(args.line); @@ -305,7 +318,7 @@ fn handleScriptHeal(server: *Server, arena: std.mem.Allocator, id: std.json.Valu replacements: []const ReplacementSpec, }; const args = browser_tools.parseArgs(Args, arena, arguments) catch { - return server.transport.sendError(id, .InvalidParams, "expected { path: string, replacements: [{ original_line, replacement_lines }] }"); + return server.sendError(id, .InvalidParams, "expected { path: string, replacements: [{ original_line, replacement_lines }] }"); }; if (!script.isPathSafe(args.path)) { @@ -367,7 +380,7 @@ fn findLineSpan(content: []const u8, line: []const u8) error{ NotFound, Ambiguou fn sendToolResultText(server: *Server, id: std.json.Value, msg: []const u8, is_error: bool) !void { const content = [_]protocol.TextContent([]const u8){.{ .text = msg }}; - try server.transport.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content, .isError = is_error }); + try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content, .isError = is_error }); } fn sendErrorContent(server: *Server, id: std.json.Value, msg: []const u8) !void {