const std = @import("std"); const model_context_encoding = @import("../shared/model_context_encoding.zig"); const tool_dispatch = @import("tool_dispatch.zig"); const tool_result_errors = @import("tool_result_errors.zig"); const Allocator = std.mem.Allocator; const SearchInput = struct { query: []u8, limit: usize, fn deinit(self: *SearchInput, alloc: Allocator) void { alloc.free(self.query); self.* = .{ .query = &.{}, .limit = 0 }; } }; const SelectInput = struct { name: []u8, fn deinit(self: *SelectInput, alloc: Allocator) void { alloc.free(self.name); self.* = .{ .name = &.{} }; } }; pub fn decodeSearch( ctx: tool_dispatch.DispatchContext, args_json: []const u8, ) tool_dispatch.DispatchError!tool_dispatch.DecodeResult { var parsed = std.json.parseFromSlice(std.json.Value, ctx.allocator, args_json, .{}) catch { return .{ .failure = try ctx.allocator.dupe(u8, "Invalid mcp_search_tools arguments.") }; }; defer parsed.deinit(); if (parsed.value != .object) { return .{ .failure = try ctx.allocator.dupe(u8, "Invalid mcp_search_tools arguments.") }; } const query_value = parsed.value.object.get("query") orelse { return .{ .failure = try ctx.allocator.dupe(u8, "mcp_search_tools requires a string query.") }; }; if (query_value != .string) { return .{ .failure = try ctx.allocator.dupe(u8, "mcp_search_tools requires a string query.") }; } const raw_limit = if (parsed.value.object.get("limit")) |value| if (value == .integer) value.integer else 0 else 0; const limit = if (raw_limit <= 0) 0 else std.math.cast(usize, raw_limit) orelse std.math.maxInt(usize); const query = try ctx.allocator.dupe(u8, query_value.string); errdefer ctx.allocator.free(query); const input = try ctx.allocator.create(SearchInput); errdefer ctx.allocator.destroy(input); input.* = .{ .query = query, .limit = limit }; return .{ .input = .{ .ptr = input, .deinit_fn = searchInputDeinit } }; } pub fn decodeSelect( ctx: tool_dispatch.DispatchContext, args_json: []const u8, ) tool_dispatch.DispatchError!tool_dispatch.DecodeResult { var parsed = std.json.parseFromSlice(std.json.Value, ctx.allocator, args_json, .{}) catch { return .{ .failure = try ctx.allocator.dupe(u8, "Invalid mcp_select_tool arguments.") }; }; defer parsed.deinit(); if (parsed.value != .object) { return .{ .failure = try ctx.allocator.dupe(u8, "Invalid mcp_select_tool arguments.") }; } const name_value = parsed.value.object.get("name") orelse { return .{ .failure = try ctx.allocator.dupe(u8, "mcp_select_tool requires an exact dynamic tool name.") }; }; if (name_value != .string) { return .{ .failure = try ctx.allocator.dupe(u8, "mcp_select_tool requires an exact dynamic tool name.") }; } const name = try ctx.allocator.dupe(u8, name_value.string); errdefer ctx.allocator.free(name); const input = try ctx.allocator.create(SelectInput); errdefer ctx.allocator.destroy(input); input.* = .{ .name = name }; return .{ .input = .{ .ptr = input, .deinit_fn = selectInputDeinit } }; } pub fn validate( _: tool_dispatch.DispatchContext, _: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!?[]u8 { return null; } pub fn callSearch( ctx: tool_dispatch.DispatchContext, erased: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { const runtime_context = ctx.mcp_ctx orelse return semanticFailure( ctx, "No MCP runtime is available.", ); const search_tools = ctx.mcp_search_tools orelse return semanticFailure( ctx, "No MCP runtime is available.", ); const input = erased.as(SearchInput); const result = search_tools( runtime_context, ctx.allocator, input.query, input.limit, ctx.mcp_permission_rules, ctx.context_limits, ctx.mcp_access, ) catch |err| { if (err == error.OutOfMemory) return error.OutOfMemory; return .{ .failure = try tool_result_errors.formatToolExecutionErrorJson( ctx.allocator, "mcp_search_tools", err, ) }; }; if (result.notice) |notice| { defer ctx.allocator.free(notice); try tool_dispatch.reportContextNotice(ctx, notice); } return .{ .success = result.model_output }; } pub fn callSelect( ctx: tool_dispatch.DispatchContext, erased: tool_dispatch.ToolInput, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { const runtime_context = ctx.mcp_ctx orelse return semanticFailure( ctx, "No MCP runtime is available.", ); const tool_schema = ctx.mcp_tool_schema orelse return semanticFailure( ctx, "No MCP runtime is available.", ); const input = erased.as(SelectInput); const schema_result = (tool_schema( runtime_context, ctx.allocator, input.name, ctx.mcp_permission_rules, ctx.context_limits, ctx.mcp_access, ) catch |err| { if (err == error.OutOfMemory) return error.OutOfMemory; return .{ .failure = try tool_result_errors.formatToolExecutionErrorJson( ctx.allocator, "mcp_select_tool", err, ) }; }) orelse { return .{ .failure = try std.fmt.allocPrint( ctx.allocator, "Dynamic MCP tool not found or not allowed: {s}", .{input.name}, ) }; }; switch (schema_result) { .rejected => |payload| { if (payload.notice) |notice| { defer ctx.allocator.free(notice); try tool_dispatch.reportContextNotice(ctx, notice); } return .{ .failure = payload.model_output }; }, .selected => |payload| { defer ctx.allocator.free(payload.model_output); defer if (payload.notice) |notice| ctx.allocator.free(notice); if (payload.notice) |notice| try tool_dispatch.reportContextNotice(ctx, notice); try tool_dispatch.reportSelectedDynamicTool(ctx, input.name, payload.model_output); }, } var success: std.Io.Writer.Allocating = .init(ctx.allocator); defer success.deinit(); success.writer.writeAll("Selected dynamic MCP tool `") catch return error.OutOfMemory; model_context_encoding.writeScalar(&success.writer, input.name) catch return error.OutOfMemory; success.writer.writeAll("`. Its executable schema will be available on the next model step; call `") catch return error.OutOfMemory; model_context_encoding.writeScalar(&success.writer, input.name) catch return error.OutOfMemory; success.writer.writeAll("` with arguments matching the selected schema.") catch return error.OutOfMemory; return .{ .success = success.toOwnedSlice() catch return error.OutOfMemory }; } pub fn readsOnly(_: tool_dispatch.ToolInput) bool { return true; } pub fn isIrreversible(_: tool_dispatch.ToolInput) bool { return false; } fn searchInputDeinit(ptr: *anyopaque, alloc: Allocator) void { const input: *SearchInput = @ptrCast(@alignCast(ptr)); input.deinit(alloc); alloc.destroy(input); } fn selectInputDeinit(ptr: *anyopaque, alloc: Allocator) void { const input: *SelectInput = @ptrCast(@alignCast(ptr)); input.deinit(alloc); alloc.destroy(input); } fn semanticFailure( ctx: tool_dispatch.DispatchContext, message: []const u8, ) tool_dispatch.DispatchError!tool_dispatch.ToolResult { return .{ .failure = try ctx.allocator.dupe(u8, message) }; }