mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-30 17:18:57 +00:00
Compare commits
22 Commits
url/resolv
...
fix/cdp-cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10b70a434e | ||
|
|
e354b7315f | ||
|
|
8723ecdd2d | ||
|
|
451178558a | ||
|
|
70dc0f6b95 | ||
|
|
d99599fa21 | ||
|
|
20e62a5551 | ||
|
|
e083d4a3d1 | ||
|
|
7a23686cbd | ||
|
|
25889ff918 | ||
|
|
b4e3f246ca | ||
|
|
8eeeeda8c1 | ||
|
|
75dc4d5b0e | ||
|
|
0d40aed1b7 | ||
|
|
78cb766298 | ||
|
|
f60e5cce6d | ||
|
|
81d4bdb157 | ||
|
|
cf5e4d7d1e | ||
|
|
9f81d7d3ff | ||
|
|
1f22462f13 | ||
|
|
273ea91378 | ||
|
|
886aa3abba |
@@ -24,6 +24,7 @@ const log = @import("log.zig");
|
|||||||
const dump = @import("browser/dump.zig");
|
const dump = @import("browser/dump.zig");
|
||||||
|
|
||||||
const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config;
|
const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config;
|
||||||
|
const mcp = @import("mcp.zig");
|
||||||
|
|
||||||
pub const RunMode = enum {
|
pub const RunMode = enum {
|
||||||
help,
|
help,
|
||||||
@@ -222,6 +223,7 @@ pub const Serve = struct {
|
|||||||
|
|
||||||
pub const Mcp = struct {
|
pub const Mcp = struct {
|
||||||
common: Common = .{},
|
common: Common = .{},
|
||||||
|
version: mcp.Version = .default,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const DumpFormat = enum {
|
pub const DumpFormat = enum {
|
||||||
@@ -453,6 +455,12 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
|||||||
\\Starts an MCP (Model Context Protocol) server over stdio
|
\\Starts an MCP (Model Context Protocol) server over stdio
|
||||||
\\Example: {s} mcp
|
\\Example: {s} mcp
|
||||||
\\
|
\\
|
||||||
|
\\Options:
|
||||||
|
\\--version
|
||||||
|
\\ Override the reported MCP version.
|
||||||
|
\\ Valid: 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25.
|
||||||
|
\\ Defaults to "2024-11-05".
|
||||||
|
\\
|
||||||
++ common_options ++
|
++ common_options ++
|
||||||
\\
|
\\
|
||||||
\\version command
|
\\version command
|
||||||
@@ -640,10 +648,22 @@ fn parseMcpArgs(
|
|||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
args: *std.process.ArgIterator,
|
args: *std.process.ArgIterator,
|
||||||
) !Mcp {
|
) !Mcp {
|
||||||
var mcp: Mcp = .{};
|
var result: Mcp = .{};
|
||||||
|
|
||||||
while (args.next()) |opt| {
|
while (args.next()) |opt| {
|
||||||
if (try parseCommonArg(allocator, opt, args, &mcp.common)) {
|
if (std.mem.eql(u8, "--version", opt)) {
|
||||||
|
const str = args.next() orelse {
|
||||||
|
log.fatal(.mcp, "missing argument value", .{ .arg = opt });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
result.version = std.meta.stringToEnum(mcp.Version, str) orelse {
|
||||||
|
log.fatal(.mcp, "invalid protocol version", .{ .value = str });
|
||||||
|
return error.InvalidArgument;
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (try parseCommonArg(allocator, opt, args, &result.common)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,7 +671,7 @@ fn parseMcpArgs(
|
|||||||
return error.UnkownOption;
|
return error.UnkownOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
return mcp;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parseFetchArgs(
|
fn parseFetchArgs(
|
||||||
|
|||||||
@@ -821,16 +821,16 @@ fn processOneMessage(self: *Client, msg: http.Handles.MultiMessage, transfer: *T
|
|||||||
break :blk std.ascii.eqlIgnoreCase(hdr.value, "close");
|
break :blk std.ascii.eqlIgnoreCase(hdr.value, "close");
|
||||||
};
|
};
|
||||||
|
|
||||||
if (msg.err != null and !is_conn_close_recv) {
|
|
||||||
transfer.requestFailed(transfer._callback_error orelse msg.err.?, true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure the transfer can't be immediately aborted from a callback
|
// make sure the transfer can't be immediately aborted from a callback
|
||||||
// since we still need it here.
|
// since we still need it here.
|
||||||
transfer._performing = true;
|
transfer._performing = true;
|
||||||
defer transfer._performing = false;
|
defer transfer._performing = false;
|
||||||
|
|
||||||
|
if (msg.err != null and !is_conn_close_recv) {
|
||||||
|
transfer.requestFailed(transfer._callback_error orelse msg.err.?, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!transfer._header_done_called) {
|
if (!transfer._header_done_called) {
|
||||||
// In case of request w/o data, we need to call the header done
|
// In case of request w/o data, we need to call the header done
|
||||||
// callback now.
|
// callback now.
|
||||||
@@ -873,7 +873,6 @@ fn processMessages(self: *Client) !bool {
|
|||||||
var processed = false;
|
var processed = false;
|
||||||
while (self.handles.readMessage()) |msg| {
|
while (self.handles.readMessage()) |msg| {
|
||||||
const transfer = try Transfer.fromConnection(&msg.conn);
|
const transfer = try Transfer.fromConnection(&msg.conn);
|
||||||
|
|
||||||
const done = self.processOneMessage(msg, transfer) catch |err| blk: {
|
const done = self.processOneMessage(msg, transfer) catch |err| blk: {
|
||||||
log.err(.http, "process_messages", .{ .err = err, .req = transfer });
|
log.err(.http, "process_messages", .{ .err = err, .req = transfer });
|
||||||
transfer.requestFailed(err, true);
|
transfer.requestFailed(err, true);
|
||||||
@@ -1068,6 +1067,24 @@ pub const Transfer = struct {
|
|||||||
if (self.req.shutdown_callback) |cb| {
|
if (self.req.shutdown_callback) |cb| {
|
||||||
cb(self.ctx);
|
cb(self.ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self._performing or self.client.performing) {
|
||||||
|
// We're currently inside of a callback. This client, and libcurl
|
||||||
|
// generally don't expect a transfer to become deinitialized during
|
||||||
|
// a callback. We can flag the transfer as aborted (which is what
|
||||||
|
// we do when transfer.abort() is called in this condition) AND,
|
||||||
|
// since this "kill()"should prevent any future callbacks, the best
|
||||||
|
// we can do is null/noop them.
|
||||||
|
self.aborted = true;
|
||||||
|
self.req.start_callback = null;
|
||||||
|
self.req.shutdown_callback = null;
|
||||||
|
self.req.header_callback = Noop.headerCallback;
|
||||||
|
self.req.data_callback = Noop.dataCallback;
|
||||||
|
self.req.done_callback = Noop.doneCallback;
|
||||||
|
self.req.error_callback = Noop.errorCallback;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.deinit();
|
self.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1492,3 +1509,12 @@ pub const Transfer = struct {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Noop = struct {
|
||||||
|
fn headerCallback(_: *Transfer) !bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
fn dataCallback(_: *Transfer, _: []const u8) !void {}
|
||||||
|
fn doneCallback(_: *anyopaque) !void {}
|
||||||
|
fn errorCallback(_: *anyopaque, _: anyerror) void {}
|
||||||
|
};
|
||||||
|
|||||||
@@ -77,41 +77,6 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
|
|||||||
return processResolved(allocator, result, opts);
|
return processResolved(allocator, result, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.len >= 4) { // Minimum: "ws:x"
|
|
||||||
if (std.mem.indexOfScalar(u8, path[0..@min(path.len, 6)], ':')) |pos| {
|
|
||||||
// we know this isn't a complete URL, else the very first check in
|
|
||||||
// this function would have handled it.
|
|
||||||
const possible_special_protocol = path[0..pos];
|
|
||||||
const special_schemes = [_][]const u8{ "https", "http", "ws", "wss", "file", "ftp" };
|
|
||||||
for (special_schemes) |special_scheme| {
|
|
||||||
if (std.ascii.eqlIgnoreCase(possible_special_protocol, special_scheme)) {
|
|
||||||
const rest = path[pos + 1 ..];
|
|
||||||
|
|
||||||
// Check if base has the same scheme
|
|
||||||
const base_scheme_end = std.mem.indexOf(u8, base, "://") orelse 0;
|
|
||||||
if (base_scheme_end > 0 and std.ascii.eqlIgnoreCase(base[0..base_scheme_end], special_scheme)) {
|
|
||||||
// Same scheme - strip it and resolve rest as relative
|
|
||||||
return resolve(allocator, base, rest, opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Different scheme - construct absolute URL
|
|
||||||
// Skip any leading slashes in rest
|
|
||||||
var rest_start: usize = 0;
|
|
||||||
while (rest_start < rest.len and (rest[rest_start] == '/' or rest[rest_start] == '\\')) {
|
|
||||||
rest_start += 1;
|
|
||||||
}
|
|
||||||
const rest_trimmed = rest[rest_start..];
|
|
||||||
|
|
||||||
// file: scheme needs empty host (triple slash)
|
|
||||||
const separator = if (std.mem.eql(u8, special_scheme, "file")) ":///" else "://";
|
|
||||||
const normalized = try std.mem.joinZ(allocator, "", &.{ special_scheme, separator, rest_trimmed });
|
|
||||||
return resolve(allocator, "", normalized, opts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Don't know what this is, just try to resolve it through our normal logic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheme_end = std.mem.indexOf(u8, base, "://");
|
const scheme_end = std.mem.indexOf(u8, base, "://");
|
||||||
const authority_start = if (scheme_end) |end| end + 3 else 0;
|
const authority_start = if (scheme_end) |end| end + 3 else 0;
|
||||||
const path_start = std.mem.indexOfScalarPos(u8, base, authority_start, '/') orelse base.len;
|
const path_start = std.mem.indexOfScalarPos(u8, base, authority_start, '/') orelse base.len;
|
||||||
@@ -1605,84 +1570,3 @@ test "URL: getOrigin" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test "URL: resolve path scheme" {
|
|
||||||
const Case = struct {
|
|
||||||
base: [:0]const u8,
|
|
||||||
path: [:0]const u8,
|
|
||||||
expected: [:0]const u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
const cases = [_]Case{
|
|
||||||
.{
|
|
||||||
.base = "https://www.example.com/example",
|
|
||||||
.path = "https:/about",
|
|
||||||
.expected = "https://www.example.com/about",
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.base = "https://www.example.com/example",
|
|
||||||
.path = "https:about",
|
|
||||||
.expected = "https://www.example.com/about",
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.base = "https://www.example.com/example",
|
|
||||||
.path = "https://about",
|
|
||||||
.expected = "https://about",
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.base = "https://www.example.com/example",
|
|
||||||
.path = "http:about",
|
|
||||||
.expected = "http://about",
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.base = "https://www.example.com/example",
|
|
||||||
.path = "http:/about",
|
|
||||||
.expected = "http://about",
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.base = "https://www.example.com/example",
|
|
||||||
.path = "http://about",
|
|
||||||
.expected = "http://about",
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.base = "https://site/",
|
|
||||||
.path = "https://path",
|
|
||||||
.expected = "https://path",
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.base = "http://localhost/",
|
|
||||||
.path = "data:test",
|
|
||||||
.expected = "data:test",
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.base = "https://www.example.com/example",
|
|
||||||
.path = "ws://about",
|
|
||||||
.expected = "ws://about",
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.base = "https://www.example.com/example",
|
|
||||||
.path = "wss://about",
|
|
||||||
.expected = "wss://about",
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.base = "https://www.example.com/example",
|
|
||||||
.path = "ftp://about",
|
|
||||||
.expected = "ftp://about",
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.base = "https://www.example.com/example",
|
|
||||||
.path = "file://path/to/file",
|
|
||||||
.expected = "file://path/to/file",
|
|
||||||
},
|
|
||||||
.{
|
|
||||||
.base = "https://www.example.com/example",
|
|
||||||
.path = "file:/path/to/file",
|
|
||||||
.expected = "file:///path/to/file",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (cases) |case| {
|
|
||||||
const result = try resolve(testing.arena_allocator, case.base, case.path, .{});
|
|
||||||
try testing.expectString(case.expected, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -148,3 +148,13 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=identity>
|
||||||
|
{
|
||||||
|
const element = document.createElement('canvas');
|
||||||
|
const ctx = element.getContext('2d');
|
||||||
|
|
||||||
|
testing.expectTrue(ctx === element.getContext('2d'));
|
||||||
|
testing.expectEqual(null, element.getContext('webgl'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -85,3 +85,13 @@
|
|||||||
loseContext.restoreContext();
|
loseContext.restoreContext();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=identity>
|
||||||
|
{
|
||||||
|
const element = document.createElement('canvas');
|
||||||
|
const ctx = element.getContext('webgl');
|
||||||
|
|
||||||
|
testing.expectTrue(ctx === element.getContext('webgl'));
|
||||||
|
testing.expectEqual(null, element.getContext('2d'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -15,8 +15,9 @@
|
|||||||
testing.expectEqual(true, validPlatforms.includes(navigator.platform));
|
testing.expectEqual(true, validPlatforms.includes(navigator.platform));
|
||||||
testing.expectEqual('en-US', navigator.language);
|
testing.expectEqual('en-US', navigator.language);
|
||||||
testing.expectEqual(true, Array.isArray(navigator.languages));
|
testing.expectEqual(true, Array.isArray(navigator.languages));
|
||||||
testing.expectEqual(1, navigator.languages.length);
|
testing.expectEqual(2, navigator.languages.length);
|
||||||
testing.expectEqual('en-US', navigator.languages[0]);
|
testing.expectEqual('en-US', navigator.languages[0]);
|
||||||
|
testing.expectEqual('en', navigator.languages[1]);
|
||||||
testing.expectEqual(true, navigator.onLine);
|
testing.expectEqual(true, navigator.onLine);
|
||||||
testing.expectEqual(true, navigator.cookieEnabled);
|
testing.expectEqual(true, navigator.cookieEnabled);
|
||||||
testing.expectEqual(true, navigator.hardwareConcurrency > 0);
|
testing.expectEqual(true, navigator.hardwareConcurrency > 0);
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ pub fn fromError(err: anyerror) ?DOMException {
|
|||||||
error.TimeoutError => .{ ._code = .timeout_error },
|
error.TimeoutError => .{ ._code = .timeout_error },
|
||||||
error.InvalidNodeType => .{ ._code = .invalid_node_type_error },
|
error.InvalidNodeType => .{ ._code = .invalid_node_type_error },
|
||||||
error.DataClone => .{ ._code = .data_clone_error },
|
error.DataClone => .{ ._code = .data_clone_error },
|
||||||
|
error.InvalidAccessError => .{ ._code = .invalid_access_error },
|
||||||
else => null,
|
else => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ pub fn getUserAgent(_: *const Navigator, page: *Page) []const u8 {
|
|||||||
return page._session.browser.app.config.http_headers.user_agent;
|
return page._session.browser.app.config.http_headers.user_agent;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getLanguages(_: *const Navigator) [1][]const u8 {
|
pub fn getLanguages(_: *const Navigator) [2][]const u8 {
|
||||||
return .{"en-US"};
|
return .{ "en-US", "en" };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getPlatform(_: *const Navigator) []const u8 {
|
pub fn getPlatform(_: *const Navigator) []const u8 {
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ const OffscreenCanvas = @import("../../canvas/OffscreenCanvas.zig");
|
|||||||
|
|
||||||
const Canvas = @This();
|
const Canvas = @This();
|
||||||
_proto: *HtmlElement,
|
_proto: *HtmlElement,
|
||||||
|
_cached: ?DrawingContext = null,
|
||||||
|
|
||||||
|
const ContextType = enum { none, @"2d", webgl };
|
||||||
|
|
||||||
pub fn asElement(self: *Canvas) *Element {
|
pub fn asElement(self: *Canvas) *Element {
|
||||||
return self._proto._proto;
|
return self._proto._proto;
|
||||||
@@ -68,17 +71,28 @@ const DrawingContext = union(enum) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext {
|
pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext {
|
||||||
if (std.mem.eql(u8, context_type, "2d")) {
|
if (self._cached) |cached| {
|
||||||
const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self });
|
const matches = switch (cached) {
|
||||||
return .{ .@"2d" = ctx };
|
.@"2d" => std.mem.eql(u8, context_type, "2d"),
|
||||||
|
.webgl => std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl"),
|
||||||
|
};
|
||||||
|
return if (matches) cached else null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) {
|
const drawing_context: DrawingContext = blk: {
|
||||||
const ctx = try page._factory.create(WebGLRenderingContext{});
|
if (std.mem.eql(u8, context_type, "2d")) {
|
||||||
return .{ .webgl = ctx };
|
const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self });
|
||||||
}
|
break :blk .{ .@"2d" = ctx };
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) {
|
||||||
|
const ctx = try page._factory.create(WebGLRenderingContext{});
|
||||||
|
break :blk .{ .webgl = ctx };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
self._cached = drawing_context;
|
||||||
|
return drawing_context;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transfers control of the canvas to an OffscreenCanvas.
|
/// Transfers control of the canvas to an OffscreenCanvas.
|
||||||
|
|||||||
@@ -363,6 +363,11 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
inspector_session: *js.Inspector.Session,
|
inspector_session: *js.Inspector.Session,
|
||||||
isolated_worlds: std.ArrayList(*IsolatedWorld),
|
isolated_worlds: std.ArrayList(*IsolatedWorld),
|
||||||
|
|
||||||
|
// Scripts registered via Page.addScriptToEvaluateOnNewDocument.
|
||||||
|
// Evaluated in each new document after navigation completes.
|
||||||
|
scripts_on_new_document: std.ArrayList(ScriptOnNewDocument) = .empty,
|
||||||
|
next_script_id: u32 = 1,
|
||||||
|
|
||||||
http_proxy_changed: bool = false,
|
http_proxy_changed: bool = false,
|
||||||
|
|
||||||
// Extra headers to add to all requests.
|
// Extra headers to add to all requests.
|
||||||
@@ -762,6 +767,11 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
/// Clients create this to be able to create variables and run code without interfering with the
|
/// Clients create this to be able to create variables and run code without interfering with the
|
||||||
/// normal namespace and values of the webpage. Similar to the main context we need to pretend to recreate it after
|
/// normal namespace and values of the webpage. Similar to the main context we need to pretend to recreate it after
|
||||||
/// a executionContextsCleared event which happens when navigating to a new page. A client can have a command be executed
|
/// a executionContextsCleared event which happens when navigating to a new page. A client can have a command be executed
|
||||||
|
const ScriptOnNewDocument = struct {
|
||||||
|
identifier: u32,
|
||||||
|
source: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
/// in the isolated world by using its Context ID or the worldName.
|
/// in the isolated world by using its Context ID or the worldName.
|
||||||
/// grantUniveralAccess Indecated whether the isolated world can reference objects like the DOM or other JS Objects.
|
/// grantUniveralAccess Indecated whether the isolated world can reference objects like the DOM or other JS Objects.
|
||||||
/// An isolated world has it's own instance of globals like Window.
|
/// An isolated world has it's own instance of globals like Window.
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
getFrameTree,
|
getFrameTree,
|
||||||
setLifecycleEventsEnabled,
|
setLifecycleEventsEnabled,
|
||||||
addScriptToEvaluateOnNewDocument,
|
addScriptToEvaluateOnNewDocument,
|
||||||
|
removeScriptToEvaluateOnNewDocument,
|
||||||
createIsolatedWorld,
|
createIsolatedWorld,
|
||||||
navigate,
|
navigate,
|
||||||
reload,
|
reload,
|
||||||
@@ -51,6 +52,7 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
.getFrameTree => return getFrameTree(cmd),
|
.getFrameTree => return getFrameTree(cmd),
|
||||||
.setLifecycleEventsEnabled => return setLifecycleEventsEnabled(cmd),
|
.setLifecycleEventsEnabled => return setLifecycleEventsEnabled(cmd),
|
||||||
.addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd),
|
.addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd),
|
||||||
|
.removeScriptToEvaluateOnNewDocument => return removeScriptToEvaluateOnNewDocument(cmd),
|
||||||
.createIsolatedWorld => return createIsolatedWorld(cmd),
|
.createIsolatedWorld => return createIsolatedWorld(cmd),
|
||||||
.navigate => return navigate(cmd),
|
.navigate => return navigate(cmd),
|
||||||
.reload => return doReload(cmd),
|
.reload => return doReload(cmd),
|
||||||
@@ -147,22 +149,55 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void {
|
|||||||
return cmd.sendResult(null, .{});
|
return cmd.sendResult(null, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: hard coded method
|
|
||||||
// With the command we receive a script we need to store and run for each new document.
|
|
||||||
// Note that the worldName refers to the name given to the isolated world.
|
|
||||||
fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void {
|
fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void {
|
||||||
// const params = (try cmd.params(struct {
|
const params = (try cmd.params(struct {
|
||||||
// source: []const u8,
|
source: []const u8,
|
||||||
// worldName: ?[]const u8 = null,
|
worldName: ?[]const u8 = null,
|
||||||
// includeCommandLineAPI: bool = false,
|
includeCommandLineAPI: bool = false,
|
||||||
// runImmediately: bool = false,
|
runImmediately: bool = false,
|
||||||
// })) orelse return error.InvalidParams;
|
})) orelse return error.InvalidParams;
|
||||||
|
|
||||||
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||||
|
|
||||||
|
if (params.runImmediately) {
|
||||||
|
log.warn(.not_implemented, "addScriptOnNewDocument", .{ .param = "runImmediately" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const script_id = bc.next_script_id;
|
||||||
|
bc.next_script_id += 1;
|
||||||
|
|
||||||
|
const source_dupe = try bc.arena.dupe(u8, params.source);
|
||||||
|
try bc.scripts_on_new_document.append(bc.arena, .{
|
||||||
|
.identifier = script_id,
|
||||||
|
.source = source_dupe,
|
||||||
|
});
|
||||||
|
|
||||||
|
var id_buf: [16]u8 = undefined;
|
||||||
|
const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{script_id}) catch "1";
|
||||||
return cmd.sendResult(.{
|
return cmd.sendResult(.{
|
||||||
.identifier = "1",
|
.identifier = id_str,
|
||||||
}, .{});
|
}, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn removeScriptToEvaluateOnNewDocument(cmd: anytype) !void {
|
||||||
|
const params = (try cmd.params(struct {
|
||||||
|
identifier: []const u8,
|
||||||
|
})) orelse return error.InvalidParams;
|
||||||
|
|
||||||
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||||
|
|
||||||
|
const target_id = std.fmt.parseInt(u32, params.identifier, 10) catch
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
|
|
||||||
|
for (bc.scripts_on_new_document.items, 0..) |script, i| {
|
||||||
|
if (script.identifier == target_id) {
|
||||||
|
_ = bc.scripts_on_new_document.orderedRemove(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
|
}
|
||||||
|
|
||||||
fn close(cmd: anytype) !void {
|
fn close(cmd: anytype) !void {
|
||||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||||
|
|
||||||
@@ -482,6 +517,27 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Evaluate scripts registered via Page.addScriptToEvaluateOnNewDocument.
|
||||||
|
// Must run after the execution context is created but before the client
|
||||||
|
// receives frameNavigated/loadEventFired so polyfills are available for
|
||||||
|
// subsequent CDP commands.
|
||||||
|
if (bc.scripts_on_new_document.items.len > 0) {
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
|
for (bc.scripts_on_new_document.items) |script| {
|
||||||
|
var try_catch: lp.js.TryCatch = undefined;
|
||||||
|
try_catch.init(&ls.local);
|
||||||
|
defer try_catch.deinit();
|
||||||
|
|
||||||
|
ls.local.eval(script.source, null) catch |err| {
|
||||||
|
const caught = try_catch.caughtOrError(arena, err);
|
||||||
|
log.warn(.cdp, "script on new doc", .{ .caught = caught });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// frameNavigated event
|
// frameNavigated event
|
||||||
try cdp.sendEvent("Page.frameNavigated", .{
|
try cdp.sendEvent("Page.frameNavigated", .{
|
||||||
.type = "Navigation",
|
.type = "Navigation",
|
||||||
@@ -840,3 +896,55 @@ test "cdp.page: reload" {
|
|||||||
try ctx.processMessage(.{ .id = 32, .method = "Page.reload", .params = .{ .ignoreCache = true } });
|
try ctx.processMessage(.{ .id = 32, .method = "Page.reload", .params = .{ .ignoreCache = true } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "cdp.page: addScriptToEvaluateOnNewDocument" {
|
||||||
|
var ctx = try testing.context();
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
var bc = try ctx.loadBrowserContext(.{ .id = "BID-9", .url = "hi.html", .target_id = "FID-000000000X".* });
|
||||||
|
|
||||||
|
{
|
||||||
|
// Register a script — should return unique identifier "1"
|
||||||
|
try ctx.processMessage(.{ .id = 20, .method = "Page.addScriptToEvaluateOnNewDocument", .params = .{ .source = "window.__test = 1" } });
|
||||||
|
try ctx.expectSentResult(.{
|
||||||
|
.identifier = "1",
|
||||||
|
}, .{ .id = 20 });
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Register another script — should return identifier "2"
|
||||||
|
try ctx.processMessage(.{ .id = 21, .method = "Page.addScriptToEvaluateOnNewDocument", .params = .{ .source = "window.__test2 = 2" } });
|
||||||
|
try ctx.expectSentResult(.{
|
||||||
|
.identifier = "2",
|
||||||
|
}, .{ .id = 21 });
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Remove the first script — should succeed
|
||||||
|
try ctx.processMessage(.{ .id = 22, .method = "Page.removeScriptToEvaluateOnNewDocument", .params = .{ .identifier = "1" } });
|
||||||
|
try ctx.expectSentResult(null, .{ .id = 22 });
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Remove a non-existent identifier — should succeed silently
|
||||||
|
try ctx.processMessage(.{ .id = 23, .method = "Page.removeScriptToEvaluateOnNewDocument", .params = .{ .identifier = "999" } });
|
||||||
|
try ctx.expectSentResult(null, .{ .id = 23 });
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
try ctx.processMessage(.{ .id = 34, .method = "Page.reload" });
|
||||||
|
// wait for this event, which is sent after we've run the registered scripts
|
||||||
|
try ctx.expectSentEvent("Page.frameNavigated", .{
|
||||||
|
.frame = .{ .loaderId = "LID-0000000002" },
|
||||||
|
}, .{});
|
||||||
|
|
||||||
|
const page = bc.session.currentPage() orelse unreachable;
|
||||||
|
|
||||||
|
var ls: js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
|
const test_val = try ls.local.exec("window.__test2", null);
|
||||||
|
try testing.expectEqual(2, try test_val.toI32());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,15 +158,20 @@ fn createTarget(cmd: anytype) !void {
|
|||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (bc.target_id != null) {
|
|
||||||
return error.TargetAlreadyLoaded;
|
|
||||||
}
|
|
||||||
if (params.browserContextId) |param_browser_context_id| {
|
if (params.browserContextId) |param_browser_context_id| {
|
||||||
if (std.mem.eql(u8, param_browser_context_id, bc.id) == false) {
|
if (std.mem.eql(u8, param_browser_context_id, bc.id) == false) {
|
||||||
return error.UnknownBrowserContextId;
|
return error.UnknownBrowserContextId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a target already exists, close it first. Lightpanda only supports
|
||||||
|
// one page at a time, so we replace the existing target rather than
|
||||||
|
// rejecting the request. This unblocks automation frameworks (e.g.
|
||||||
|
// Stagehand) that call createTarget multiple times.
|
||||||
|
if (bc.target_id != null) {
|
||||||
|
try doCloseTarget(cmd, bc);
|
||||||
|
}
|
||||||
|
|
||||||
// if target_id is null, we should never have a page
|
// if target_id is null, we should never have a page
|
||||||
lp.assert(bc.session.page == null, "CDP.target.createTarget not null page", .{});
|
lp.assert(bc.session.page == null, "CDP.target.createTarget not null page", .{});
|
||||||
|
|
||||||
@@ -280,34 +285,9 @@ fn closeTarget(cmd: anytype) !void {
|
|||||||
return error.UnknownTargetId;
|
return error.UnknownTargetId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// can't be null if we have a target_id
|
|
||||||
lp.assert(bc.session.page != null, "CDP.target.closeTarget null page", .{});
|
|
||||||
|
|
||||||
try cmd.sendResult(.{ .success = true }, .{ .include_session_id = false });
|
try cmd.sendResult(.{ .success = true }, .{ .include_session_id = false });
|
||||||
|
|
||||||
// could be null, created but never attached
|
try doCloseTarget(cmd, bc);
|
||||||
if (bc.session_id) |session_id| {
|
|
||||||
// Inspector.detached event
|
|
||||||
try cmd.sendEvent("Inspector.detached", .{
|
|
||||||
.reason = "Render process gone.",
|
|
||||||
}, .{ .session_id = session_id });
|
|
||||||
|
|
||||||
// detachedFromTarget event
|
|
||||||
try cmd.sendEvent("Target.detachedFromTarget", .{
|
|
||||||
.targetId = target_id,
|
|
||||||
.sessionId = session_id,
|
|
||||||
.reason = "Render process gone.",
|
|
||||||
}, .{});
|
|
||||||
|
|
||||||
bc.session_id = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
bc.session.removePage();
|
|
||||||
for (bc.isolated_worlds.items) |world| {
|
|
||||||
world.deinit();
|
|
||||||
}
|
|
||||||
bc.isolated_worlds.clearRetainingCapacity();
|
|
||||||
bc.target_id = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getTargetInfo(cmd: anytype) !void {
|
fn getTargetInfo(cmd: anytype) !void {
|
||||||
@@ -468,6 +448,41 @@ fn setAutoAttach(cmd: anytype) !void {
|
|||||||
try cmd.sendResult(null, .{});
|
try cmd.sendResult(null, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Close the current target in a browser context: send detach events,
|
||||||
|
/// remove the page, clean up isolated worlds, and clear the target_id.
|
||||||
|
/// Shared by closeTarget and createTarget (which auto-closes before
|
||||||
|
/// creating a replacement).
|
||||||
|
fn doCloseTarget(cmd: anytype, bc: anytype) !void {
|
||||||
|
// can't be null if we have a target_id
|
||||||
|
lp.assert(bc.session.page != null, "CDP.target.doCloseTarget null page", .{});
|
||||||
|
|
||||||
|
// could be null, created but never attached
|
||||||
|
if (bc.session_id) |session_id| {
|
||||||
|
try cmd.sendEvent("Inspector.detached", .{
|
||||||
|
.reason = "Render process gone.",
|
||||||
|
}, .{ .session_id = session_id });
|
||||||
|
|
||||||
|
try cmd.sendEvent("Target.detachedFromTarget", .{
|
||||||
|
.targetId = &bc.target_id.?,
|
||||||
|
.sessionId = session_id,
|
||||||
|
.reason = "Render process gone.",
|
||||||
|
}, .{});
|
||||||
|
|
||||||
|
bc.session_id = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try cmd.sendEvent("Target.targetDestroyed", .{
|
||||||
|
.targetId = &bc.target_id.?,
|
||||||
|
}, .{});
|
||||||
|
|
||||||
|
bc.session.removePage();
|
||||||
|
for (bc.isolated_worlds.items) |world| {
|
||||||
|
world.deinit();
|
||||||
|
}
|
||||||
|
bc.isolated_worlds.clearRetainingCapacity();
|
||||||
|
bc.target_id = null;
|
||||||
|
}
|
||||||
|
|
||||||
fn doAttachtoTarget(cmd: anytype, target_id: []const u8) !void {
|
fn doAttachtoTarget(cmd: anytype, target_id: []const u8) !void {
|
||||||
const bc = cmd.browser_context.?;
|
const bc = cmd.browser_context.?;
|
||||||
const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next();
|
const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next();
|
||||||
@@ -646,6 +661,7 @@ test "cdp.target: closeTarget" {
|
|||||||
{
|
{
|
||||||
try ctx.processMessage(.{ .id = 11, .method = "Target.closeTarget", .params = .{ .targetId = "TID-000000000A" } });
|
try ctx.processMessage(.{ .id = 11, .method = "Target.closeTarget", .params = .{ .targetId = "TID-000000000A" } });
|
||||||
try ctx.expectSentResult(.{ .success = true }, .{ .id = 11 });
|
try ctx.expectSentResult(.{ .success = true }, .{ .id = 11 });
|
||||||
|
try ctx.expectSentEvent("Target.targetDestroyed", .{ .targetId = "TID-000000000A" }, .{});
|
||||||
try testing.expectEqual(null, bc.session.page);
|
try testing.expectEqual(null, bc.session.page);
|
||||||
try testing.expectEqual(null, bc.target_id);
|
try testing.expectEqual(null, bc.target_id);
|
||||||
}
|
}
|
||||||
@@ -771,6 +787,53 @@ test "cdp.target: detachFromTarget" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "cdp.target: createTarget closes existing target (issue #1962)" {
|
||||||
|
var ctx = try testing.context();
|
||||||
|
defer ctx.deinit();
|
||||||
|
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||||
|
{
|
||||||
|
// Create first target
|
||||||
|
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
|
||||||
|
try testing.expectEqual(true, bc.target_id != null);
|
||||||
|
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
|
||||||
|
|
||||||
|
// Create second target — should succeed by auto-closing the first
|
||||||
|
try ctx.processMessage(.{ .id = 11, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
|
||||||
|
try ctx.expectSentEvent("Target.targetDestroyed", .{ .targetId = "FID-0000000001" }, .{});
|
||||||
|
try testing.expectEqual(true, bc.target_id != null);
|
||||||
|
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 11 });
|
||||||
|
|
||||||
|
// Page should exist (new target is active)
|
||||||
|
try testing.expectEqual(true, bc.session.page != null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "cdp.target: createTarget closes existing attached target (issue #1962)" {
|
||||||
|
var ctx = try testing.context();
|
||||||
|
defer ctx.deinit();
|
||||||
|
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||||
|
{
|
||||||
|
// Create and attach first target
|
||||||
|
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
|
||||||
|
try testing.expectEqual(true, bc.target_id != null);
|
||||||
|
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
|
||||||
|
|
||||||
|
try ctx.processMessage(.{ .id = 11, .method = "Target.attachToTarget", .params = .{ .targetId = bc.target_id.? } });
|
||||||
|
const session_id = bc.session_id.?;
|
||||||
|
try ctx.expectSentResult(.{ .sessionId = session_id }, .{ .id = 11 });
|
||||||
|
|
||||||
|
// Create second target — should close and detach the first
|
||||||
|
try ctx.processMessage(.{ .id = 12, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
|
||||||
|
// Should have sent detach events for the old session
|
||||||
|
try ctx.expectSentEvent("Inspector.detached", .{ .reason = "Render process gone." }, .{ .session_id = session_id });
|
||||||
|
try ctx.expectSentEvent("Target.detachedFromTarget", .{ .sessionId = session_id, .reason = "Render process gone." }, .{});
|
||||||
|
try ctx.expectSentEvent("Target.targetDestroyed", .{ .targetId = "FID-0000000001" }, .{});
|
||||||
|
// New target should be created
|
||||||
|
try testing.expectEqual(true, bc.target_id != null);
|
||||||
|
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 12 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "cdp.target: detachFromTarget without session" {
|
test "cdp.target: detachFromTarget without session" {
|
||||||
var ctx = try testing.context();
|
var ctx = try testing.context();
|
||||||
defer ctx.deinit();
|
defer ctx.deinit();
|
||||||
|
|||||||
@@ -168,13 +168,26 @@ const TestContext = struct {
|
|||||||
index: ?usize = null,
|
index: ?usize = null,
|
||||||
};
|
};
|
||||||
pub fn expectSent(self: *TestContext, expected: anytype, opts: SentOpts) !void {
|
pub fn expectSent(self: *TestContext, expected: anytype, opts: SentOpts) !void {
|
||||||
const serialized = try json.Stringify.valueAlloc(base.arena_allocator, expected, .{
|
const expected_json = blk: {
|
||||||
.whitespace = .indent_2,
|
// Zig makes this hard. When sendJSON is called, we're sending an anytype.
|
||||||
.emit_null_optional_fields = false,
|
// We can't record that in an ArrayList(???), so we serialize it to JSON.
|
||||||
});
|
// Now, ideally, we could just take our expected structure, serialize it to
|
||||||
|
// json and check if the two are equal.
|
||||||
|
// Except serializing to JSON isn't deterministic.
|
||||||
|
// So we serialize the JSON then we deserialize to json.Value. And then we can
|
||||||
|
// compare our anytype expectation with the json.Value that we captured
|
||||||
|
|
||||||
|
const serialized = try json.Stringify.valueAlloc(base.arena_allocator, expected, .{
|
||||||
|
.whitespace = .indent_2,
|
||||||
|
.emit_null_optional_fields = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
break :blk try std.json.parseFromSliceLeaky(json.Value, base.arena_allocator, serialized, .{});
|
||||||
|
};
|
||||||
|
|
||||||
for (0..5) |_| {
|
for (0..5) |_| {
|
||||||
for (self.received.items, 0..) |received, i| {
|
for (self.received.items, 0..) |received, i| {
|
||||||
if (try compareExpectedToSent(serialized, received) == false) {
|
if (try base.isEqualJson(expected_json, received) == false) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +200,15 @@ const TestContext = struct {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self.cdp_) |*cdp__| {
|
||||||
|
if (cdp__.browser_context) |*bc| {
|
||||||
|
if (bc.session.page != null) {
|
||||||
|
var runner = try bc.session.runner(.{});
|
||||||
|
_ = try runner.tick(.{ .ms = 1000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
std.Thread.sleep(5 * std.time.ns_per_ms);
|
std.Thread.sleep(5 * std.time.ns_per_ms);
|
||||||
try self.read();
|
try self.read();
|
||||||
}
|
}
|
||||||
@@ -299,17 +321,3 @@ pub fn context() !TestContext {
|
|||||||
.socket = pair[0],
|
.socket = pair[0],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zig makes this hard. When sendJSON is called, we're sending an anytype.
|
|
||||||
// We can't record that in an ArrayList(???), so we serialize it to JSON.
|
|
||||||
// Now, ideally, we could just take our expected structure, serialize it to
|
|
||||||
// json and check if the two are equal.
|
|
||||||
// Except serializing to JSON isn't deterministic.
|
|
||||||
// So we serialize the JSON then we deserialize to json.Value. And then we can
|
|
||||||
// compare our anytype expectation with the json.Value that we captured
|
|
||||||
|
|
||||||
fn compareExpectedToSent(expected: []const u8, actual: json.Value) !bool {
|
|
||||||
const expected_value = try std.json.parseFromSlice(json.Value, std.testing.allocator, expected, .{});
|
|
||||||
defer expected_value.deinit();
|
|
||||||
return base.isEqualJson(expected_value.value, actual);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
pub const protocol = @import("mcp/protocol.zig");
|
pub const protocol = @import("mcp/protocol.zig");
|
||||||
|
pub const Version = protocol.Version;
|
||||||
pub const router = @import("mcp/router.zig");
|
pub const router = @import("mcp/router.zig");
|
||||||
pub const Server = @import("mcp/Server.zig");
|
pub const Server = @import("mcp/Server.zig");
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ test "MCP.Server - Integration: synchronous smoke test" {
|
|||||||
|
|
||||||
try router.processRequests(server, &in_reader);
|
try router.processRequests(server, &in_reader);
|
||||||
|
|
||||||
try testing.expectJson(.{ .jsonrpc = "2.0", .id = 1 }, out_alloc.writer.buffered());
|
try testing.expectJson(.{ .jsonrpc = "2.0", .id = 1, .result = .{ .protocolVersion = "2024-11-05" } }, out_alloc.writer.buffered());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "MCP.Server - Integration: ping request returns an empty result" {
|
test "MCP.Server - Integration: ping request returns an empty result" {
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const Version = enum {
|
||||||
|
@"2024-11-05",
|
||||||
|
@"2025-03-26",
|
||||||
|
@"2025-06-18",
|
||||||
|
@"2025-11-25",
|
||||||
|
|
||||||
|
pub const default: Version = .@"2024-11-05";
|
||||||
|
};
|
||||||
|
|
||||||
pub const Request = struct {
|
pub const Request = struct {
|
||||||
jsonrpc: []const u8 = "2.0",
|
jsonrpc: []const u8 = "2.0",
|
||||||
id: ?std.json.Value = null,
|
id: ?std.json.Value = null,
|
||||||
|
|||||||
@@ -81,8 +81,12 @@ pub fn handleMessage(server: *Server, arena: std.mem.Allocator, msg: []const u8)
|
|||||||
|
|
||||||
fn handleInitialize(server: *Server, req: protocol.Request) !void {
|
fn handleInitialize(server: *Server, req: protocol.Request) !void {
|
||||||
const id = req.id orelse return;
|
const id = req.id orelse return;
|
||||||
const result = protocol.InitializeResult{
|
const version: protocol.Version = switch (server.app.config.mode) {
|
||||||
.protocolVersion = "2025-11-25",
|
.mcp => |opts| opts.version,
|
||||||
|
else => .default,
|
||||||
|
};
|
||||||
|
const result: protocol.InitializeResult = .{
|
||||||
|
.protocolVersion = @tagName(version),
|
||||||
.capabilities = .{
|
.capabilities = .{
|
||||||
.resources = .{},
|
.resources = .{},
|
||||||
.tools = .{},
|
.tools = .{},
|
||||||
@@ -121,7 +125,7 @@ test "MCP.router - handleMessage - synchronous unit tests" {
|
|||||||
\\{"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"}}}
|
||||||
);
|
);
|
||||||
try testing.expectJson(
|
try testing.expectJson(
|
||||||
\\{ "jsonrpc": "2.0", "id": 1, "result": { "capabilities": { "tools": {} } } }
|
\\{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": { "tools": {} } } }
|
||||||
, out_alloc.writer.buffered());
|
, out_alloc.writer.buffered());
|
||||||
out_alloc.writer.end = 0;
|
out_alloc.writer.end = 0;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user