diff --git a/src/mcp/Server.zig b/src/mcp/Server.zig index 02e99abc..2df4b792 100644 --- a/src/mcp/Server.zig +++ b/src/mcp/Server.zig @@ -16,9 +16,6 @@ browser: *lp.Browser, session: *lp.Session, page: *lp.Page, -tools: []const protocol.Tool, -resources: []const protocol.Resource, - is_running: std.atomic.Value(bool) = .init(false), stdout_mutex: std.Thread.Mutex = .{}, @@ -44,117 +41,9 @@ pub fn init(allocator: std.mem.Allocator, app: *App) !*Self { self.session = try self.browser.newSession(self.notification); self.page = try self.session.createPage(); - self.tools = try initTools(allocator); - self.resources = try initResources(allocator); - return self; } -fn initTools(allocator: std.mem.Allocator) ![]const protocol.Tool { - const tools = try allocator.alloc(protocol.Tool, 6); - errdefer allocator.free(tools); - - tools[0] = .{ - .name = "goto", - .description = "Navigate to a specified URL and load the page in memory so it can be reused later for info extraction.", - .inputSchema = try std.json.parseFromSliceLeaky(std.json.Value, allocator, - \\{ - \\ "type": "object", - \\ "properties": { - \\ "url": { "type": "string", "description": "The URL to navigate to, must be a valid URL." } - \\ }, - \\ "required": ["url"] - \\} - , .{}), - }; - tools[1] = .{ - .name = "search", - .description = "Use a search engine to look for specific words, terms, sentences. The search page will then be loaded in memory.", - .inputSchema = try std.json.parseFromSliceLeaky(std.json.Value, allocator, - \\{ - \\ "type": "object", - \\ "properties": { - \\ "text": { "type": "string", "description": "The text to search for, must be a valid search query." } - \\ }, - \\ "required": ["text"] - \\} - , .{}), - }; - tools[2] = .{ - .name = "markdown", - .description = "Get the page content in markdown format. If a url is provided, it navigates to that url first.", - .inputSchema = try std.json.parseFromSliceLeaky(std.json.Value, allocator, - \\{ - \\ "type": "object", - \\ "properties": { - \\ "url": { "type": "string", "description": "Optional URL to navigate to before fetching markdown." } - \\ } - \\} - , .{}), - }; - tools[3] = .{ - .name = "links", - .description = "Extract all links in the opened page. If a url is provided, it navigates to that url first.", - .inputSchema = try std.json.parseFromSliceLeaky(std.json.Value, allocator, - \\{ - \\ "type": "object", - \\ "properties": { - \\ "url": { "type": "string", "description": "Optional URL to navigate to before extracting links." } - \\ } - \\} - , .{}), - }; - tools[4] = .{ - .name = "evaluate", - .description = "Evaluate JavaScript in the current page context. If a url is provided, it navigates to that url first.", - .inputSchema = try std.json.parseFromSliceLeaky(std.json.Value, allocator, - \\{ - \\ "type": "object", - \\ "properties": { - \\ "script": { "type": "string" }, - \\ "url": { "type": "string", "description": "Optional URL to navigate to before evaluating." } - \\ }, - \\ "required": ["script"] - \\} - , .{}), - }; - tools[5] = .{ - .name = "over", - .description = "Used to indicate that the task is over and give the final answer if there is any. This is the last tool to be called in a task.", - .inputSchema = try std.json.parseFromSliceLeaky(std.json.Value, allocator, - \\{ - \\ "type": "object", - \\ "properties": { - \\ "result": { "type": "string", "description": "The final result of the task." } - \\ }, - \\ "required": ["result"] - \\} - , .{}), - }; - - return tools; -} - -fn initResources(allocator: std.mem.Allocator) ![]const protocol.Resource { - const resources = try allocator.alloc(protocol.Resource, 2); - errdefer allocator.free(resources); - - resources[0] = .{ - .uri = "mcp://page/html", - .name = "Page HTML", - .description = "The serialized HTML DOM of the current page", - .mimeType = "text/html", - }; - resources[1] = .{ - .uri = "mcp://page/markdown", - .name = "Page Markdown", - .description = "The token-efficient markdown representation of the current page", - .mimeType = "text/markdown", - }; - - return resources; -} - pub fn deinit(self: *Self) void { self.is_running.store(false, .release); @@ -163,9 +52,6 @@ pub fn deinit(self: *Self) void { self.notification.deinit(); self.http_client.deinit(); - self.allocator.free(self.tools); - self.allocator.free(self.resources); - self.allocator.destroy(self); } diff --git a/src/mcp/protocol.zig b/src/mcp/protocol.zig index 44a85d33..827b9099 100644 --- a/src/mcp/protocol.zig +++ b/src/mcp/protocol.zig @@ -94,7 +94,17 @@ pub const ToolsCapability = struct { pub const Tool = struct { name: []const u8, description: ?[]const u8 = null, - inputSchema: std.json.Value, + inputSchema: RawJson, +}; + +pub const RawJson = struct { + json: []const u8, + + pub fn jsonStringify(self: @This(), jw: anytype) !void { + try jw.beginWriteRaw(); + try jw.writer.writeAll(self.json); + jw.endWriteRaw(); + } }; pub const Resource = struct { diff --git a/src/mcp/resources.zig b/src/mcp/resources.zig index bbffdd8a..7b5ef6bc 100644 --- a/src/mcp/resources.zig +++ b/src/mcp/resources.zig @@ -6,11 +6,26 @@ const log = lp.log; const protocol = @import("protocol.zig"); const Server = @import("Server.zig"); +pub const resource_list = [_]protocol.Resource{ + .{ + .uri = "mcp://page/html", + .name = "Page HTML", + .description = "The serialized HTML DOM of the current page", + .mimeType = "text/html", + }, + .{ + .uri = "mcp://page/markdown", + .name = "Page Markdown", + .description = "The token-efficient markdown representation of the current page", + .mimeType = "text/markdown", + }, +}; + pub fn handleList(server: *Server, req: protocol.Request) !void { const result = struct { resources: []const protocol.Resource, }{ - .resources = server.resources, + .resources = &resource_list, }; try server.sendResult(req.id.?, result); diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index eba5003a..0f8a7693 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -9,12 +9,92 @@ const Selector = @import("../browser/webapi/selector/Selector.zig"); const protocol = @import("protocol.zig"); const Server = @import("Server.zig"); +pub const tool_list = [_]protocol.Tool{ + .{ + .name = "goto", + .description = "Navigate to a specified URL and load the page in memory so it can be reused later for info extraction.", + .inputSchema = .{ .json = + \\{ + \\ "type": "object", + \\ "properties": { + \\ "url": { "type": "string", "description": "The URL to navigate to, must be a valid URL." } + \\ }, + \\ "required": ["url"] + \\} + }, + }, + .{ + .name = "search", + .description = "Use a search engine to look for specific words, terms, sentences. The search page will then be loaded in memory.", + .inputSchema = .{ .json = + \\{ + \\ "type": "object", + \\ "properties": { + \\ "text": { "type": "string", "description": "The text to search for, must be a valid search query." } + \\ }, + \\ "required": ["text"] + \\} + }, + }, + .{ + .name = "markdown", + .description = "Get the page content in markdown format. If a url is provided, it navigates to that url first.", + .inputSchema = .{ .json = + \\{ + \\ "type": "object", + \\ "properties": { + \\ "url": { "type": "string", "description": "Optional URL to navigate to before fetching markdown." } + \\ } + \\} + }, + }, + .{ + .name = "links", + .description = "Extract all links in the opened page. If a url is provided, it navigates to that url first.", + .inputSchema = .{ .json = + \\{ + \\ "type": "object", + \\ "properties": { + \\ "url": { "type": "string", "description": "Optional URL to navigate to before extracting links." } + \\ } + \\} + }, + }, + .{ + .name = "evaluate", + .description = "Evaluate JavaScript in the current page context. If a url is provided, it navigates to that url first.", + .inputSchema = .{ .json = + \\{ + \\ "type": "object", + \\ "properties": { + \\ "script": { "type": "string" }, + \\ "url": { "type": "string", "description": "Optional URL to navigate to before evaluating." } + \\ }, + \\ "required": ["script"] + \\} + }, + }, + .{ + .name = "over", + .description = "Used to indicate that the task is over and give the final answer if there is any. This is the last tool to be called in a task.", + .inputSchema = .{ .json = + \\{ + \\ "type": "object", + \\ "properties": { + \\ "result": { "type": "string", "description": "The final result of the task." } + \\ }, + \\ "required": ["result"] + \\} + }, + }, +}; + pub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void { _ = arena; const result = struct { tools: []const protocol.Tool, }{ - .tools = server.tools, + .tools = &tool_list, }; try server.sendResult(req.id.?, result);