Files
browser/src/mcp/router.zig
2026-03-03 14:40:20 +09:00

144 lines
4.7 KiB
Zig

const std = @import("std");
const lp = @import("lightpanda");
const protocol = @import("protocol.zig");
const resources = @import("resources.zig");
const Server = @import("Server.zig");
const tools = @import("tools.zig");
pub fn processRequests(server: *Server, reader: *std.io.Reader) !void {
var arena: std.heap.ArenaAllocator = .init(server.allocator);
defer arena.deinit();
while (true) {
_ = arena.reset(.retain_capacity);
const aa = arena.allocator();
const buffered_line = reader.takeDelimiter('\n') catch |err| switch (err) {
error.StreamTooLong => {
log.err(.mcp, "Message too long", .{});
continue;
},
else => return err,
} orelse break;
const trimmed = std.mem.trim(u8, buffered_line, " \r\t");
if (trimmed.len > 0) {
handleMessage(server, aa, trimmed) catch |err| {
log.err(.mcp, "Failed to handle message", .{ .err = err, .msg = trimmed });
};
}
}
}
const log = @import("../log.zig");
const Method = enum {
initialize,
@"notifications/initialized",
@"tools/list",
@"tools/call",
@"resources/list",
@"resources/read",
};
const method_map = std.StaticStringMap(Method).initComptime(.{
.{ "initialize", .initialize },
.{ "notifications/initialized", .@"notifications/initialized" },
.{ "tools/list", .@"tools/list" },
.{ "tools/call", .@"tools/call" },
.{ "resources/list", .@"resources/list" },
.{ "resources/read", .@"resources/read" },
});
pub fn handleMessage(server: *Server, arena: std.mem.Allocator, msg: []const u8) !void {
const req = std.json.parseFromSliceLeaky(protocol.Request, arena, msg, .{
.ignore_unknown_fields = true,
}) catch |err| {
log.warn(.mcp, "JSON Parse Error", .{ .err = err, .msg = msg });
try server.sendError(.null, .ParseError, "Parse error");
return;
};
const method = method_map.get(req.method) orelse {
if (req.id != null) {
try server.sendError(req.id.?, .MethodNotFound, "Method not found");
}
return;
};
switch (method) {
.initialize => try handleInitialize(server, req),
.@"notifications/initialized" => {},
.@"tools/list" => try tools.handleList(server, arena, req),
.@"tools/call" => try tools.handleCall(server, arena, req),
.@"resources/list" => try resources.handleList(server, req),
.@"resources/read" => try resources.handleRead(server, arena, req),
}
}
fn handleInitialize(server: *Server, req: protocol.Request) !void {
const result = protocol.InitializeResult{
.protocolVersion = "2025-11-25",
.capabilities = .{
.resources = .{},
.tools = .{},
},
.serverInfo = .{
.name = "lightpanda",
.version = "0.1.0",
},
};
try server.sendResult(req.id.?, result);
}
const testing = @import("../testing.zig");
test "MCP.router - handleMessage - synchronous unit tests" {
defer testing.reset();
const allocator = testing.allocator;
const app = testing.test_app;
var out_alloc: std.io.Writer.Allocating = .init(testing.arena_allocator);
defer out_alloc.deinit();
var server = try Server.init(allocator, app, &out_alloc.writer);
defer server.deinit();
const aa = testing.arena_allocator;
// 1. Valid handshake
try handleMessage(server, aa,
\\{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}}
);
try testing.expectJson(
\\{ "id": 1, "result": { "capabilities": { "tools": {} } } }
, out_alloc.writer.buffered());
out_alloc.writer.end = 0;
// 2. Tools list
try handleMessage(server, aa,
\\{"jsonrpc":"2.0","id":2,"method":"tools/list"}
);
try testing.expectJson(.{ .id = 2 }, out_alloc.writer.buffered());
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.expectJson(.{ .id = 3, .@"error" = .{ .code = -32601 } }, out_alloc.writer.buffered());
out_alloc.writer.end = 0;
// 4. Parse error
{
const old_filter = log.opts.filter_scopes;
log.opts.filter_scopes = &.{.mcp};
defer log.opts.filter_scopes = old_filter;
try handleMessage(server, aa, "invalid json");
try testing.expectJson("{\"id\": null, \"error\": {\"code\": -32700}}", out_alloc.writer.buffered());
}
}