PranitInitial commit439f83c3 months agoHistory
363 lines · 14.3 KB · Zig
RawDownload
const std = @import("std");const tool_dispatch = @import("tool_dispatch.zig");const tool_mcp_runtime = @import("tool_mcp_runtime.zig");const tool_result_errors = @import("tool_result_errors.zig"); const Allocator = std.mem.Allocator;const max_arguments_json_bytes: usize = 64 * 1024;const max_context_arguments: usize = 128;const max_context_bytes: usize = 128 * 1024;const input_required_failure = "{\"error\":\"McpInputRequired\"}"; pub fn isInputRequiredFailure(body: []const u8) bool {    return std.mem.eql(u8, body, input_required_failure);} const Input = struct {    request: tool_mcp_runtime.FeatureRequest,    server_name: []u8,    identity: []u8,    argument_name: []u8,    value: []u8,    arguments_json: []u8,    context_arguments: []tool_mcp_runtime.FeatureArgument,     fn deinit(self: *Input, alloc: Allocator) void {        alloc.free(self.server_name);        alloc.free(self.identity);        alloc.free(self.argument_name);        alloc.free(self.value);        alloc.free(self.arguments_json);        for (self.context_arguments) |argument| {            alloc.free(argument.name);            alloc.free(argument.value);        }        alloc.free(self.context_arguments);        self.* = undefined;    }}; pub fn decode(    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 |err| switch (err) {        error.OutOfMemory => return error.OutOfMemory,        else => return failure(ctx.allocator, "Invalid mcp_features arguments."),    };    defer parsed.deinit();    if (parsed.value != .object) return failure(ctx.allocator, "Invalid mcp_features arguments.");    if (!onlyKnownFields(parsed.value.object)) {        return failure(ctx.allocator, "mcp_features received an unknown argument field.");    }     const action_text = stringField(parsed.value.object, "action") orelse        return failure(ctx.allocator, "mcp_features requires an action.");    const action = parseAction(action_text) orelse        return failure(ctx.allocator, "mcp_features action is not supported.");    const server_name = stringField(parsed.value.object, "server") orelse        return failure(ctx.allocator, "mcp_features requires a stable server name.");    if (server_name.len == 0) {        return failure(ctx.allocator, "mcp_features requires a stable server name.");    }     const identity = switch (action) {        .resource_list, .resource_templates, .prompt_list => "",        .resource_read => stringField(parsed.value.object, "uri") orelse            return failure(ctx.allocator, "resource_read requires uri."),        .resource_complete => stringField(parsed.value.object, "uri_template") orelse            return failure(ctx.allocator, "resource_complete requires uri_template."),        .prompt_get, .prompt_complete => stringField(parsed.value.object, "prompt") orelse            return failure(ctx.allocator, "prompt actions require prompt."),    };    if (actionNeedsIdentity(action) and identity.len == 0) {        return failure(ctx.allocator, "mcp_features requires a stable feature identity.");    }    const argument_name = if (action == .prompt_complete or action == .resource_complete)        stringField(parsed.value.object, "argument") orelse            return failure(ctx.allocator, "completion actions require argument.")    else        "";    if ((action == .prompt_complete or action == .resource_complete) and argument_name.len == 0) {        return failure(ctx.allocator, "completion actions require argument.");    }    const value = stringField(parsed.value.object, "value") orelse "";     const arguments_json = argumentsJson(ctx.allocator, parsed.value.object, action) catch |err| switch (err) {        error.OutOfMemory => return error.OutOfMemory,        else => return failure(ctx.allocator, "mcp_features arguments are invalid or too large."),    };    errdefer ctx.allocator.free(arguments_json);    const context_arguments = contextArguments(ctx.allocator, parsed.value.object, action) catch |err| switch (err) {        error.OutOfMemory => return error.OutOfMemory,        else => return failure(ctx.allocator, "mcp_features completion context is invalid or too large."),    };    errdefer freeContextArguments(ctx.allocator, context_arguments);    const owned_server = try ctx.allocator.dupe(u8, server_name);    errdefer ctx.allocator.free(owned_server);    const owned_identity = try ctx.allocator.dupe(u8, identity);    errdefer ctx.allocator.free(owned_identity);    const owned_argument = try ctx.allocator.dupe(u8, argument_name);    errdefer ctx.allocator.free(owned_argument);    const owned_value = try ctx.allocator.dupe(u8, value);    errdefer ctx.allocator.free(owned_value);    const input = try ctx.allocator.create(Input);    errdefer ctx.allocator.destroy(input);    input.* = .{        .request = .{            .action = action,            .server_name = owned_server,            .identity = owned_identity,            .argument_name = owned_argument,            .value = owned_value,            .arguments_json = arguments_json,            .context_arguments = context_arguments,        },        .server_name = owned_server,        .identity = owned_identity,        .argument_name = owned_argument,        .value = owned_value,        .arguments_json = arguments_json,        .context_arguments = context_arguments,    };    return .{ .input = .{ .ptr = input, .deinit_fn = inputDeinit } };} pub fn validate(    _: tool_dispatch.DispatchContext,    _: tool_dispatch.ToolInput,) tool_dispatch.DispatchError!?[]u8 {    return null;} pub fn call(    ctx: tool_dispatch.DispatchContext,    erased: tool_dispatch.ToolInput,) tool_dispatch.DispatchError!tool_dispatch.ToolResult {    const runtime_context = ctx.mcp_ctx orelse        return semanticFailure(ctx.allocator, "No MCP runtime is available.");    const call_feature = ctx.mcp_call_feature orelse        return semanticFailure(ctx.allocator, "MCP resources and prompts are unavailable.");    const result = call_feature(runtime_context, ctx.allocator, erased.as(Input).request, .{        .cancel_flag = ctx.cancel_flag,        .input_responder = ctx.mcp_input_responder,        .output_limit_bytes = ctx.max_tool_result_bytes,        .access = ctx.mcp_access,    }) catch |err| {        if (err == error.OutOfMemory) return error.OutOfMemory;        if (err == error.McpInputRequired or            err == error.UnsupportedMode or            err == error.UnsupportedInputRequest)        {            return .{ .failure = try ctx.allocator.dupe(u8, input_required_failure) };        }        return .{ .failure = try tool_result_errors.formatToolExecutionErrorJson(            ctx.allocator,            "mcp_features",            err,        ) };    };    return .{ .success = result.model_output };} pub fn readsOnly(_: tool_dispatch.ToolInput) bool {    return true;} pub fn isIrreversible(_: tool_dispatch.ToolInput) bool {    return false;} fn inputDeinit(raw: *anyopaque, alloc: Allocator) void {    const input: *Input = @ptrCast(@alignCast(raw));    input.deinit(alloc);    alloc.destroy(input);} fn onlyKnownFields(object: std.json.ObjectMap) bool {    var iterator = object.iterator();    while (iterator.next()) |entry| {        const name = entry.key_ptr.*;        if (!std.mem.eql(u8, name, "action") and            !std.mem.eql(u8, name, "server") and            !std.mem.eql(u8, name, "uri") and            !std.mem.eql(u8, name, "uri_template") and            !std.mem.eql(u8, name, "prompt") and            !std.mem.eql(u8, name, "argument") and            !std.mem.eql(u8, name, "value") and            !std.mem.eql(u8, name, "arguments") and            !std.mem.eql(u8, name, "context"))        {            return false;        }    }    return true;} fn stringField(object: std.json.ObjectMap, name: []const u8) ?[]const u8 {    const value = object.get(name) orelse return null;    if (value != .string) return null;    return value.string;} fn parseAction(value: []const u8) ?tool_mcp_runtime.FeatureAction {    inline for (std.meta.fields(tool_mcp_runtime.FeatureAction)) |field| {        if (std.mem.eql(u8, value, field.name)) return @enumFromInt(field.value);    }    return null;} fn actionNeedsIdentity(action: tool_mcp_runtime.FeatureAction) bool {    return switch (action) {        .resource_list, .resource_templates, .prompt_list => false,        else => true,    };} fn argumentsJson(    alloc: Allocator,    object: std.json.ObjectMap,    action: tool_mcp_runtime.FeatureAction,) ![]u8 {    const value = object.get("arguments");    if (action != .prompt_get) {        if (value != null) return error.InvalidToolArguments;        return alloc.dupe(u8, "{}");    }    const arguments = value orelse return alloc.dupe(u8, "{}");    if (arguments != .object) return error.InvalidToolArguments;    var iterator = arguments.object.iterator();    while (iterator.next()) |entry| {        if (entry.value_ptr.* != .string) return error.InvalidToolArguments;    }    var out: std.Io.Writer.Allocating = .init(alloc);    defer out.deinit();    try std.json.Stringify.value(arguments, .{}, &out.writer);    if (out.written().len > max_arguments_json_bytes) return error.InvalidToolArguments;    return out.toOwnedSlice();} fn contextArguments(    alloc: Allocator,    object: std.json.ObjectMap,    action: tool_mcp_runtime.FeatureAction,) ![]tool_mcp_runtime.FeatureArgument {    const value = object.get("context");    const completion = action == .prompt_complete or action == .resource_complete;    if (!completion) {        if (value != null) return error.InvalidToolArguments;        return alloc.alloc(tool_mcp_runtime.FeatureArgument, 0);    }    const context = value orelse return alloc.alloc(tool_mcp_runtime.FeatureArgument, 0);    if (context != .object or context.object.count() > max_context_arguments) {        return error.InvalidToolArguments;    }    const result = try alloc.alloc(tool_mcp_runtime.FeatureArgument, context.object.count());    var count: usize = 0;    var total_bytes: usize = 0;    errdefer {        for (result[0..count]) |argument| {            alloc.free(argument.name);            alloc.free(argument.value);        }        alloc.free(result);    }    var iterator = context.object.iterator();    while (iterator.next()) |entry| {        if (entry.value_ptr.* != .string) return error.InvalidToolArguments;        total_bytes = std.math.add(usize, total_bytes, entry.key_ptr.*.len) catch            return error.InvalidToolArguments;        total_bytes = std.math.add(usize, total_bytes, entry.value_ptr.string.len) catch            return error.InvalidToolArguments;        if (total_bytes > max_context_bytes) return error.InvalidToolArguments;        const name = try alloc.dupe(u8, entry.key_ptr.*);        errdefer alloc.free(name);        const argument_value = try alloc.dupe(u8, entry.value_ptr.string);        result[count] = .{ .name = name, .value = argument_value };        count += 1;    }    return result;} fn freeContextArguments(alloc: Allocator, arguments: []tool_mcp_runtime.FeatureArgument) void {    for (arguments) |argument| {        alloc.free(argument.name);        alloc.free(argument.value);    }    alloc.free(arguments);} fn failure(alloc: Allocator, message: []const u8) Allocator.Error!tool_dispatch.DecodeResult {    return .{ .failure = try alloc.dupe(u8, message) };} fn semanticFailure(alloc: Allocator, message: []const u8) Allocator.Error!tool_dispatch.ToolResult {    return .{ .failure = try alloc.dupe(u8, message) };} test "MCP feature tool decodes stable server-qualified resource and prompt requests" {    const alloc = std.testing.allocator;    const ctx = tool_dispatch.DispatchContext{ .allocator = alloc };    const decoded = try decode(ctx, "{\"action\":\"prompt_get\",\"server\":\"fixture\",\"prompt\":\"review\",\"arguments\":{\"tone\":\"brief\"}}");    var input = switch (decoded) {        .input => |value| value,        .failure => |message| {            defer alloc.free(message);            return error.TestUnexpectedResult;        },    };    defer input.deinit(alloc);    const typed = input.as(Input).request;    try std.testing.expectEqual(tool_mcp_runtime.FeatureAction.prompt_get, typed.action);    try std.testing.expectEqualStrings("fixture", typed.server_name);    try std.testing.expectEqualStrings("review", typed.identity);    try std.testing.expectEqualStrings("{\"tone\":\"brief\"}", typed.arguments_json);} test "MCP feature tool owned input releases every allocation failure" {    const Case = struct {        fn run(alloc: Allocator) !void {            const decoded = try decode(.{ .allocator = alloc }, "{\"action\":\"resource_complete\",\"server\":\"fixture\",\"uri_template\":\"custom:///{path}\",\"argument\":\"path\",\"context\":{\"root\":\"src\"}}");            switch (decoded) {                .input => |input| input.deinit(alloc),                .failure => |message| alloc.free(message),            }        }    };    try std.testing.checkAllAllocationFailures(std.testing.allocator, Case.run, .{});} test "MCP feature input-required remains a typed terminal result" {    const Fixture = struct {        fn callFeature(            _: *anyopaque,            _: Allocator,            _: tool_mcp_runtime.FeatureRequest,            _: tool_mcp_runtime.FeatureCallOptions,        ) anyerror!tool_mcp_runtime.FeatureResult {            return error.McpInputRequired;        }    };    const alloc = std.testing.allocator;    var marker: u8 = 0;    const ctx = tool_dispatch.DispatchContext{        .allocator = alloc,        .mcp_ctx = @ptrCast(&marker),        .mcp_call_feature = Fixture.callFeature,    };    const decoded = try decode(ctx, "{\"action\":\"resource_read\",\"server\":\"fixture\",\"uri\":\"custom://item\"}");    var input = switch (decoded) {        .input => |value| value,        .failure => |message| {            defer alloc.free(message);            return error.TestUnexpectedResult;        },    };    defer input.deinit(alloc);    var result = try call(ctx, input);    defer result.deinit(alloc);    switch (result) {        .failure => |body| try std.testing.expect(isInputRequiredFailure(body)),        .success => return error.TestUnexpectedResult,    }}