From 6e7c8d7ae27c4921dda4cbb5fd42b3c748f2716d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Mon, 2 Mar 2026 22:18:02 +0900 Subject: [PATCH] mcp: consolidate tests and streamline parameter parsing --- src/mcp/Server.zig | 10 ++----- src/mcp/resources.zig | 16 ---------- src/mcp/router.zig | 20 +++++++++---- src/mcp/tools.zig | 70 +++++++++++-------------------------------- 4 files changed, 35 insertions(+), 81 deletions(-) diff --git a/src/mcp/Server.zig b/src/mcp/Server.zig index b76075fb..360f2cb5 100644 --- a/src/mcp/Server.zig +++ b/src/mcp/Server.zig @@ -89,21 +89,17 @@ test "MCP Integration: synchronous smoke test" { const input = \\{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}} - \\{"jsonrpc":"2.0","id":2,"method":"tools/list"} ; - var in_reader: std.io.Reader = .fixed(input); - var out_alloc: std.io.Writer.Allocating = .init(allocator); + var in_reader = std.io.Reader.fixed(input); + var out_alloc = std.io.Writer.Allocating.init(allocator); defer out_alloc.deinit(); - var server: *Self = try .init(allocator, app, &out_alloc.writer); + var server = try Self.init(allocator, app, &out_alloc.writer); defer server.deinit(); try router.processRequests(server, &in_reader); const output = out_alloc.writer.buffered(); try testing.expect(std.mem.indexOf(u8, output, "\"id\":1") != null); - try testing.expect(std.mem.indexOf(u8, output, "\"tools\":{}") != null); - try testing.expect(std.mem.indexOf(u8, output, "\"id\":2") != null); - try testing.expect(std.mem.indexOf(u8, output, "\"name\":\"goto\"") != null); } diff --git a/src/mcp/resources.zig b/src/mcp/resources.zig index 2a08feb9..7fd59dc3 100644 --- a/src/mcp/resources.zig +++ b/src/mcp/resources.zig @@ -106,19 +106,3 @@ pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Reque } const testing = @import("../testing.zig"); - -test "resource_list contains expected resources" { - try testing.expect(resource_list.len >= 2); - try testing.expectString("mcp://page/html", resource_list[0].uri); - try testing.expectString("mcp://page/markdown", resource_list[1].uri); -} - -test "ReadParams parsing" { - var arena = std.heap.ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const aa = arena.allocator(); - - const raw = "{\"uri\": \"mcp://page/html\"}"; - const parsed = try std.json.parseFromSlice(ReadParams, aa, raw, .{}); - try testing.expectString("mcp://page/html", parsed.value.uri); -} diff --git a/src/mcp/router.zig b/src/mcp/router.zig index 7b21a37b..368db601 100644 --- a/src/mcp/router.zig +++ b/src/mcp/router.zig @@ -108,23 +108,31 @@ test "handleMessage - synchronous unit tests" { defer arena.deinit(); const aa = arena.allocator(); - // 1. Valid request + // 1. Valid handshake try handleMessage(server, aa, - \\{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}} + \\{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}} ); try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "\"id\":1") != null); - try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "\"name\":\"lightpanda\"") != null); + try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "\"tools\":{}") != null); out_alloc.writer.end = 0; - // 2. Method not found + // 2. Tools list try handleMessage(server, aa, - \\{"jsonrpc":"2.0","id":2,"method":"unknown_method"} + \\{"jsonrpc":"2.0","id":2,"method":"tools/list"} ); try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "\"id\":2") != null); + try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "\"name\":\"goto\"") != null); + out_alloc.writer.end = 0; + + // 3. Method not found + try handleMessage(server, aa, + \\{"jsonrpc":"2.0","id":3,"method":"unknown_method"} + ); + try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "\"id\":3") != null); try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "\"code\":-32601") != null); out_alloc.writer.end = 0; - // 3. Parse error + // 4. Parse error { const old_filter = log.opts.filter_scopes; log.opts.filter_scopes = &.{.mcp}; diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index 8a113142..456e531a 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -201,7 +201,7 @@ pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Reque } fn handleGoto(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { - const args = try parseParams(GotoParams, arena, arguments, server, id, "goto"); + const args = try parseArguments(GotoParams, arena, arguments, server, id, "goto"); try performGoto(server, args.url, id); const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = "Navigated successfully." }}; @@ -209,7 +209,7 @@ fn handleGoto(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arg } fn handleSearch(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { - const args = try parseParams(SearchParams, arena, arguments, server, id, "search"); + const args = try parseArguments(SearchParams, arena, arguments, server, id, "search"); const component: std.Uri.Component = .{ .raw = args.text }; var url_aw = std.Io.Writer.Allocating.init(arena); @@ -230,10 +230,12 @@ fn handleMarkdown(server: *Server, arena: std.mem.Allocator, id: std.json.Value, const MarkdownParams = struct { url: ?[:0]const u8 = null, }; - if (try parseParamsOptional(MarkdownParams, arena, arguments)) |args| { - if (args.url) |u| { - try performGoto(server, u, id); - } + if (arguments) |args_raw| { + if (std.json.parseFromValueLeaky(MarkdownParams, arena, args_raw, .{ .ignore_unknown_fields = true })) |args| { + if (args.url) |u| { + try performGoto(server, u, id); + } + } else |_| {} } const result = struct { @@ -251,10 +253,12 @@ fn handleLinks(server: *Server, arena: std.mem.Allocator, id: std.json.Value, ar const LinksParams = struct { url: ?[:0]const u8 = null, }; - if (try parseParamsOptional(LinksParams, arena, arguments)) |args| { - if (args.url) |u| { - try performGoto(server, u, id); - } + if (arguments) |args_raw| { + if (std.json.parseFromValueLeaky(LinksParams, arena, args_raw, .{ .ignore_unknown_fields = true })) |args| { + if (args.url) |u| { + try performGoto(server, u, id); + } + } else |_| {} } const result = struct { @@ -269,7 +273,7 @@ fn handleLinks(server: *Server, arena: std.mem.Allocator, id: std.json.Value, ar } fn handleEvaluate(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { - const args = try parseParams(EvaluateParams, arena, arguments, server, id, "evaluate"); + const args = try parseArguments(EvaluateParams, arena, arguments, server, id, "evaluate"); if (args.url) |url| { try performGoto(server, url, id); @@ -291,16 +295,15 @@ fn handleEvaluate(server: *Server, arena: std.mem.Allocator, id: std.json.Value, } fn handleOver(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void { - const args = try parseParams(OverParams, arena, arguments, server, id, "over"); + const args = try parseArguments(OverParams, arena, arguments, server, id, "over"); const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = args.result }}; try server.sendResult(id, .{ .content = &content }); } -fn parseParams(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value, server: *Server, id: std.json.Value, tool_name: []const u8) !T { +fn parseArguments(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value, server: *Server, id: std.json.Value, tool_name: []const u8) !T { if (arguments == null) { - const msg = std.fmt.allocPrint(arena, "Missing arguments for {s}", .{tool_name}) catch "Missing arguments"; - try server.sendError(id, .InvalidParams, msg); + try server.sendError(id, .InvalidParams, "Missing arguments"); return error.InvalidParams; } return std.json.parseFromValueLeaky(T, arena, arguments.?, .{ .ignore_unknown_fields = true }) catch { @@ -310,15 +313,6 @@ fn parseParams(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json. }; } -fn parseParamsOptional(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value) !?T { - if (arguments) |args_raw| { - if (std.json.parseFromValueLeaky(T, arena, args_raw, .{ .ignore_unknown_fields = true })) |args| { - return args; - } else |_| {} - } - return null; -} - fn performGoto(server: *Server, url: [:0]const u8, id: std.json.Value) !void { _ = server.page.navigate(url, .{ .reason = .address_bar, @@ -332,31 +326,3 @@ fn performGoto(server: *Server, url: [:0]const u8, id: std.json.Value) !void { } const testing = @import("../testing.zig"); - -test "tool_list contains expected tools" { - try testing.expect(tool_list.len >= 6); - try testing.expectString("goto", tool_list[0].name); -} - -test "parseParams - valid" { - var arena = std.heap.ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const aa = arena.allocator(); - - const arguments = try std.json.parseFromSlice(std.json.Value, aa, "{\"url\": \"https://example.com\"}", .{}); - - const args = try parseParamsOptional(GotoParams, aa, arguments.value); - try testing.expect(args != null); - try testing.expectString("https://example.com", args.?.url); -} - -test "parseParams - invalid" { - var arena = std.heap.ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const aa = arena.allocator(); - - const arguments = try std.json.parseFromSlice(std.json.Value, aa, "{\"not_url\": \"foo\"}", .{}); - - const args = try parseParamsOptional(GotoParams, aa, arguments.value); - try testing.expect(args == null); -}