mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
mcp: consolidate tests and streamline parameter parsing
This commit is contained in:
@@ -89,21 +89,17 @@ test "MCP Integration: synchronous smoke test" {
|
|||||||
|
|
||||||
const input =
|
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":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 in_reader = std.io.Reader.fixed(input);
|
||||||
var out_alloc: std.io.Writer.Allocating = .init(allocator);
|
var out_alloc = std.io.Writer.Allocating.init(allocator);
|
||||||
defer out_alloc.deinit();
|
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();
|
defer server.deinit();
|
||||||
|
|
||||||
try router.processRequests(server, &in_reader);
|
try router.processRequests(server, &in_reader);
|
||||||
|
|
||||||
const output = out_alloc.writer.buffered();
|
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, "\"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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,19 +106,3 @@ pub fn handleRead(server: *Server, arena: std.mem.Allocator, req: protocol.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -108,23 +108,31 @@ test "handleMessage - synchronous unit tests" {
|
|||||||
defer arena.deinit();
|
defer arena.deinit();
|
||||||
const aa = arena.allocator();
|
const aa = arena.allocator();
|
||||||
|
|
||||||
// 1. Valid request
|
// 1. Valid handshake
|
||||||
try handleMessage(server, aa,
|
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(), "\"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;
|
out_alloc.writer.end = 0;
|
||||||
|
|
||||||
// 2. Method not found
|
// 2. Tools list
|
||||||
try handleMessage(server, aa,
|
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(), "\"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);
|
try testing.expect(std.mem.indexOf(u8, out_alloc.writer.buffered(), "\"code\":-32601") != null);
|
||||||
out_alloc.writer.end = 0;
|
out_alloc.writer.end = 0;
|
||||||
|
|
||||||
// 3. Parse error
|
// 4. Parse error
|
||||||
{
|
{
|
||||||
const old_filter = log.opts.filter_scopes;
|
const old_filter = log.opts.filter_scopes;
|
||||||
log.opts.filter_scopes = &.{.mcp};
|
log.opts.filter_scopes = &.{.mcp};
|
||||||
|
|||||||
@@ -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 {
|
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);
|
try performGoto(server, args.url, id);
|
||||||
|
|
||||||
const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = "Navigated successfully." }};
|
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 {
|
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 };
|
const component: std.Uri.Component = .{ .raw = args.text };
|
||||||
var url_aw = std.Io.Writer.Allocating.init(arena);
|
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 {
|
const MarkdownParams = struct {
|
||||||
url: ?[:0]const u8 = null,
|
url: ?[:0]const u8 = null,
|
||||||
};
|
};
|
||||||
if (try parseParamsOptional(MarkdownParams, arena, arguments)) |args| {
|
if (arguments) |args_raw| {
|
||||||
if (args.url) |u| {
|
if (std.json.parseFromValueLeaky(MarkdownParams, arena, args_raw, .{ .ignore_unknown_fields = true })) |args| {
|
||||||
try performGoto(server, u, id);
|
if (args.url) |u| {
|
||||||
}
|
try performGoto(server, u, id);
|
||||||
|
}
|
||||||
|
} else |_| {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = struct {
|
const result = struct {
|
||||||
@@ -251,10 +253,12 @@ fn handleLinks(server: *Server, arena: std.mem.Allocator, id: std.json.Value, ar
|
|||||||
const LinksParams = struct {
|
const LinksParams = struct {
|
||||||
url: ?[:0]const u8 = null,
|
url: ?[:0]const u8 = null,
|
||||||
};
|
};
|
||||||
if (try parseParamsOptional(LinksParams, arena, arguments)) |args| {
|
if (arguments) |args_raw| {
|
||||||
if (args.url) |u| {
|
if (std.json.parseFromValueLeaky(LinksParams, arena, args_raw, .{ .ignore_unknown_fields = true })) |args| {
|
||||||
try performGoto(server, u, id);
|
if (args.url) |u| {
|
||||||
}
|
try performGoto(server, u, id);
|
||||||
|
}
|
||||||
|
} else |_| {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = struct {
|
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 {
|
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| {
|
if (args.url) |url| {
|
||||||
try performGoto(server, url, id);
|
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 {
|
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 }};
|
const content = [_]struct { type: []const u8, text: []const u8 }{.{ .type = "text", .text = args.result }};
|
||||||
try server.sendResult(id, .{ .content = &content });
|
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) {
|
if (arguments == null) {
|
||||||
const msg = std.fmt.allocPrint(arena, "Missing arguments for {s}", .{tool_name}) catch "Missing arguments";
|
try server.sendError(id, .InvalidParams, "Missing arguments");
|
||||||
try server.sendError(id, .InvalidParams, msg);
|
|
||||||
return error.InvalidParams;
|
return error.InvalidParams;
|
||||||
}
|
}
|
||||||
return std.json.parseFromValueLeaky(T, arena, arguments.?, .{ .ignore_unknown_fields = true }) catch {
|
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 {
|
fn performGoto(server: *Server, url: [:0]const u8, id: std.json.Value) !void {
|
||||||
_ = server.page.navigate(url, .{
|
_ = server.page.navigate(url, .{
|
||||||
.reason = .address_bar,
|
.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");
|
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);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user