mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
286 lines
8.1 KiB
Zig
286 lines
8.1 KiB
Zig
const std = @import("std");
|
|
|
|
pub const Request = struct {
|
|
jsonrpc: []const u8 = "2.0",
|
|
id: ?std.json.Value = null,
|
|
method: []const u8,
|
|
params: ?std.json.Value = null,
|
|
};
|
|
|
|
pub const Response = struct {
|
|
jsonrpc: []const u8 = "2.0",
|
|
id: std.json.Value,
|
|
result: ?std.json.Value = null,
|
|
@"error": ?Error = null,
|
|
};
|
|
|
|
pub const Error = struct {
|
|
code: i64,
|
|
message: []const u8,
|
|
data: ?std.json.Value = null,
|
|
};
|
|
|
|
pub const ErrorCode = enum(i64) {
|
|
ParseError = -32700,
|
|
InvalidRequest = -32600,
|
|
MethodNotFound = -32601,
|
|
InvalidParams = -32602,
|
|
InternalError = -32603,
|
|
};
|
|
|
|
pub const Notification = struct {
|
|
jsonrpc: []const u8 = "2.0",
|
|
method: []const u8,
|
|
params: ?std.json.Value = null,
|
|
};
|
|
|
|
// Core MCP Types mapping to official specification
|
|
pub const InitializeRequest = struct {
|
|
jsonrpc: []const u8 = "2.0",
|
|
id: std.json.Value,
|
|
method: []const u8 = "initialize",
|
|
params: InitializeParams,
|
|
};
|
|
|
|
pub const InitializeParams = struct {
|
|
protocolVersion: []const u8,
|
|
capabilities: Capabilities,
|
|
clientInfo: Implementation,
|
|
};
|
|
|
|
pub const Capabilities = struct {
|
|
experimental: ?std.json.Value = null,
|
|
roots: ?RootsCapability = null,
|
|
sampling: ?SamplingCapability = null,
|
|
};
|
|
|
|
pub const RootsCapability = struct {
|
|
listChanged: ?bool = null,
|
|
};
|
|
|
|
pub const SamplingCapability = struct {};
|
|
|
|
pub const Implementation = struct {
|
|
name: []const u8,
|
|
version: []const u8,
|
|
};
|
|
|
|
pub const InitializeResult = struct {
|
|
protocolVersion: []const u8,
|
|
capabilities: ServerCapabilities,
|
|
serverInfo: Implementation,
|
|
};
|
|
|
|
pub const ServerCapabilities = struct {
|
|
experimental: ?std.json.Value = null,
|
|
logging: ?LoggingCapability = null,
|
|
prompts: ?PromptsCapability = null,
|
|
resources: ?ResourcesCapability = null,
|
|
tools: ?ToolsCapability = null,
|
|
};
|
|
|
|
pub const LoggingCapability = struct {};
|
|
pub const PromptsCapability = struct {
|
|
listChanged: ?bool = null,
|
|
};
|
|
pub const ResourcesCapability = struct {
|
|
subscribe: ?bool = null,
|
|
listChanged: ?bool = null,
|
|
};
|
|
pub const ToolsCapability = struct {
|
|
listChanged: ?bool = null,
|
|
};
|
|
|
|
pub const Tool = struct {
|
|
name: []const u8,
|
|
description: ?[]const u8 = null,
|
|
inputSchema: []const u8,
|
|
|
|
pub fn jsonStringify(self: @This(), jw: anytype) !void {
|
|
try jw.beginObject();
|
|
try jw.objectField("name");
|
|
try jw.write(self.name);
|
|
if (self.description) |d| {
|
|
try jw.objectField("description");
|
|
try jw.write(d);
|
|
}
|
|
try jw.objectField("inputSchema");
|
|
_ = try jw.beginWriteRaw();
|
|
try jw.writer.writeAll(self.inputSchema);
|
|
jw.endWriteRaw();
|
|
try jw.endObject();
|
|
}
|
|
};
|
|
|
|
pub fn minify(comptime json: []const u8) []const u8 {
|
|
return comptime blk: {
|
|
var res: []const u8 = "";
|
|
var in_string = false;
|
|
var escaped = false;
|
|
for (json) |c| {
|
|
if (in_string) {
|
|
res = res ++ [1]u8{c};
|
|
if (escaped) {
|
|
escaped = false;
|
|
} else if (c == '\\') {
|
|
escaped = true;
|
|
} else if (c == '"') {
|
|
in_string = false;
|
|
}
|
|
} else {
|
|
switch (c) {
|
|
' ', '\n', '\r', '\t' => continue,
|
|
'"' => {
|
|
in_string = true;
|
|
res = res ++ [1]u8{c};
|
|
},
|
|
else => res = res ++ [1]u8{c},
|
|
}
|
|
}
|
|
}
|
|
break :blk res;
|
|
};
|
|
}
|
|
|
|
pub const Resource = struct {
|
|
uri: []const u8,
|
|
name: []const u8,
|
|
description: ?[]const u8 = null,
|
|
mimeType: ?[]const u8 = null,
|
|
};
|
|
|
|
pub const JsonEscapingWriter = struct {
|
|
inner_writer: *std.Io.Writer,
|
|
writer: std.Io.Writer,
|
|
|
|
pub fn init(inner_writer: *std.Io.Writer) JsonEscapingWriter {
|
|
return .{
|
|
.inner_writer = inner_writer,
|
|
.writer = .{
|
|
.vtable = &vtable,
|
|
.buffer = &.{},
|
|
},
|
|
};
|
|
}
|
|
|
|
const vtable = std.Io.Writer.VTable{
|
|
.drain = drain,
|
|
};
|
|
|
|
fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {
|
|
const self: *JsonEscapingWriter = @alignCast(@fieldParentPtr("writer", w));
|
|
var total: usize = 0;
|
|
for (data[0 .. data.len - 1]) |slice| {
|
|
std.json.Stringify.encodeJsonStringChars(slice, .{}, self.inner_writer) catch return error.WriteFailed;
|
|
total += slice.len;
|
|
}
|
|
const pattern = data[data.len - 1];
|
|
for (0..splat) |_| {
|
|
std.json.Stringify.encodeJsonStringChars(pattern, .{}, self.inner_writer) catch return error.WriteFailed;
|
|
total += pattern.len;
|
|
}
|
|
return total;
|
|
}
|
|
};
|
|
|
|
const testing = @import("../testing.zig");
|
|
|
|
test "protocol request parsing" {
|
|
const raw_json =
|
|
\\{
|
|
\\ "jsonrpc": "2.0",
|
|
\\ "id": 1,
|
|
\\ "method": "initialize",
|
|
\\ "params": {
|
|
\\ "protocolVersion": "2024-11-05",
|
|
\\ "capabilities": {},
|
|
\\ "clientInfo": {
|
|
\\ "name": "test-client",
|
|
\\ "version": "1.0.0"
|
|
\\ }
|
|
\\ }
|
|
\\}
|
|
;
|
|
|
|
const parsed = try std.json.parseFromSlice(Request, testing.allocator, raw_json, .{ .ignore_unknown_fields = true });
|
|
defer parsed.deinit();
|
|
|
|
const req = parsed.value;
|
|
try testing.expectString("2.0", req.jsonrpc);
|
|
try testing.expectString("initialize", req.method);
|
|
try testing.expect(req.id.? == .integer);
|
|
try testing.expectEqual(@as(i64, 1), req.id.?.integer);
|
|
try testing.expect(req.params != null);
|
|
|
|
// Test nested parsing of InitializeParams
|
|
const init_params = try std.json.parseFromValue(InitializeParams, testing.allocator, req.params.?, .{ .ignore_unknown_fields = true });
|
|
defer init_params.deinit();
|
|
|
|
try testing.expectString("2024-11-05", init_params.value.protocolVersion);
|
|
try testing.expectString("test-client", init_params.value.clientInfo.name);
|
|
try testing.expectString("1.0.0", init_params.value.clientInfo.version);
|
|
}
|
|
|
|
test "protocol response formatting" {
|
|
const response = Response{
|
|
.id = .{ .integer = 42 },
|
|
.result = .{ .string = "success" },
|
|
};
|
|
|
|
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
|
defer aw.deinit();
|
|
try std.json.Stringify.value(response, .{ .emit_null_optional_fields = false }, &aw.writer);
|
|
|
|
try testing.expectString("{\"jsonrpc\":\"2.0\",\"id\":42,\"result\":\"success\"}", aw.written());
|
|
}
|
|
|
|
test "protocol error formatting" {
|
|
const response = Response{
|
|
.id = .{ .string = "abc" },
|
|
.@"error" = .{
|
|
.code = @intFromEnum(ErrorCode.MethodNotFound),
|
|
.message = "Method not found",
|
|
},
|
|
};
|
|
|
|
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
|
defer aw.deinit();
|
|
try std.json.Stringify.value(response, .{ .emit_null_optional_fields = false }, &aw.writer);
|
|
|
|
try testing.expectString("{\"jsonrpc\":\"2.0\",\"id\":\"abc\",\"error\":{\"code\":-32601,\"message\":\"Method not found\"}}", aw.written());
|
|
}
|
|
|
|
test "JsonEscapingWriter" {
|
|
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
|
defer aw.deinit();
|
|
|
|
var escaping_writer = JsonEscapingWriter.init(&aw.writer);
|
|
|
|
// test newlines and quotes
|
|
try escaping_writer.writer.writeAll("hello\n\"world\"");
|
|
|
|
// the writer outputs escaped string chars without surrounding quotes
|
|
try testing.expectString("hello\\n\\\"world\\\"", aw.written());
|
|
}
|
|
|
|
test "Tool serialization" {
|
|
const t = Tool{
|
|
.name = "test",
|
|
.inputSchema = minify(
|
|
\\{
|
|
\\ "type": "object",
|
|
\\ "properties": {
|
|
\\ "foo": { "type": "string" }
|
|
\\ }
|
|
\\}
|
|
),
|
|
};
|
|
|
|
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
|
defer aw.deinit();
|
|
|
|
try std.json.Stringify.value(t, .{}, &aw.writer);
|
|
|
|
try testing.expectString("{\"name\":\"test\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"foo\":{\"type\":\"string\"}}}}", aw.written());
|
|
}
|