一个Zig编写的MCP调用命令行工具

本文档说明 MCP streamable-http 命令行工具的 Zig 语言实现。

功能概述

mcp-tool-zig 是一个用于调用 MCP 服务的轻量级命令行客户端,传输协议使用 streamable-http。当前支持三个命令:

  • list_tools:打印所有工具的 namedescription
  • get_tool_schema <tool_name>:打印指定工具的完整 schema,输出为格式化 JSON。
  • call_tool <tool_name> <json_arguments>:调用指定工具,并打印工具返回的 JSON 结果。

当前实现使用 Zig 标准库。为了减小可执行文件体积,代码中明确禁用了 HTTPS/TLS,仅支持 http:// MCP 服务地址。

代码结构

build.zig

Zig 0.16.0 的构建配置文件。

当前使用的主要体积优化配置如下:

const optimize: std.builtin.OptimizeMode = .ReleaseSmall;

exe.root_module.strip = true;
exe.root_module.single_threaded = true;
exe.root_module.omit_frame_pointer = true;
exe.root_module.error_tracing = false;
exe.root_module.stack_protector = false;
exe.root_module.stack_check = false;
exe.root_module.unwind_tables = .none;

这些配置会生成较小的 release 版本二进制文件,并移除普通命令行使用场景中不需要的调试信息、栈展开信息和部分安全检查代码。

代码

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize: std.builtin.OptimizeMode = .ReleaseSmall;

    const module = b.createModule(.{
        .root_source_file = b.path("main.zig"),
        .target = target,
        .optimize = optimize,
    });

    const exe = b.addExecutable(.{
        .name = "mcp-tool-zig",
        .root_module = module,
    });

    exe.root_module.strip = true;
    exe.root_module.single_threaded = true;
    exe.root_module.omit_frame_pointer = true;
    exe.root_module.error_tracing = false;
    exe.root_module.stack_protector = false;
    exe.root_module.stack_check = false;
    exe.root_module.unwind_tables = .none;

    b.installArtifact(exe);
}

main.zig

主程序实现文件。

主要模块和函数说明:

  • std_options:禁用 std.http.Client 的 TLS 支持,避免 HTTPS/证书相关代码进入最终可执行文件。
  • Client:封装 MCP JSON-RPC 请求逻辑,并保存 MCP session 状态。
  • initialize:发送 MCP initializenotifications/initialized 请求。
  • listTools:调用 MCP tools/list 接口。
  • callTool:调用 MCP tools/call 接口。
  • resolveEndpointURL:按照命令行参数、环境变量、注册表、默认值的顺序解析 MCP 服务地址。
  • queryRegistryString:通过 Win32 API 读取 Windows 注册表配置。
  • printToolSummaries:用于 list_tools,只打印工具 namedescription
  • printToolSchema:用于 get_tool_schema,打印指定工具的完整 schema,输出为格式化 JSON。
  • printPrettyJSON:用于打印工具调用结果,输出为格式化 JSON。
  • ensureUtf8:校验请求和响应内容是否为合法 UTF-8。

代码

const std = @import("std");
const builtin = @import("builtin");

pub const std_options: std.Options = .{
    .http_disable_tls = true,
};

const protocol_version = "2025-06-18";
const default_endpoint_url = "http://127.0.0.1:8080/mcp";

const AppError = error{
    Usage,
    UnsupportedURLScheme,
    InvalidURL,
    InvalidHTTPResponse,
    HTTPStatusError,
    EmptySSEResponse,
    MissingJSONRPCResult,
    ToolNotFound,
    InvalidUTF8,
};

const Endpoint = struct {
    host: []const u8,
    port: u16,
    path: []const u8,
};

const HTTPResponse = struct {
    status: u16,
    headers: []const u8,
    body: []const u8,
};

const Client = struct {
    allocator: std.mem.Allocator,
    io: std.Io,
    endpoint_url: []const u8,
    session_id: ?[]u8 = null,
    next_id: u64 = 0,

    fn deinit(self: *Client) void {
        if (self.session_id) |sid| self.allocator.free(sid);
    }

    fn initialize(self: *Client) !void {
        const params =
            \\{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"mcp-tool-zig","version":"0.1.0"}}
        ;
        const result = try self.request("initialize", params);
        self.allocator.free(result);

        try self.notify("notifications/initialized", "{}");
    }

    fn listTools(self: *Client) ![]u8 {
        return self.request("tools/list", "{}");
    }

    fn callTool(self: *Client, name: []const u8, args_json: []const u8) ![]u8 {
        var params: std.Io.Writer.Allocating = .init(self.allocator);
        defer params.deinit();

        try params.writer.writeAll("{\"name\":");
        try params.writer.print("{f}", .{std.json.fmt(name, .{})});
        try params.writer.writeAll(",\"arguments\":");
        try params.writer.writeAll(args_json);
        try params.writer.writeByte('}');

        return self.request("tools/call", params.written());
    }

    fn request(self: *Client, method: []const u8, params_json: []const u8) ![]u8 {
        self.next_id += 1;

        var body: std.Io.Writer.Allocating = .init(self.allocator);
        defer body.deinit();

        try body.writer.print("{{\"jsonrpc\":\"2.0\",\"id\":{}", .{self.next_id});
        try body.writer.writeAll(",\"method\":");
        try body.writer.print("{f}", .{std.json.fmt(method, .{})});
        try body.writer.writeAll(",\"params\":");
        try body.writer.writeAll(params_json);
        try body.writer.writeByte('}');

        const response_json = try self.postJSON(body.written(), true);
        defer self.allocator.free(response_json);
        return try self.extractResult(response_json);
    }

    fn notify(self: *Client, method: []const u8, params_json: []const u8) !void {
        var body: std.Io.Writer.Allocating = .init(self.allocator);
        defer body.deinit();

        try body.writer.writeAll("{\"jsonrpc\":\"2.0\",\"method\":");
        try body.writer.print("{f}", .{std.json.fmt(method, .{})});
        try body.writer.writeAll(",\"params\":");
        try body.writer.writeAll(params_json);
        try body.writer.writeByte('}');

        const response = try self.postJSON(body.written(), false);
        self.allocator.free(response);
    }

    fn postJSON(self: *Client, body: []const u8, want_body: bool) ![]u8 {
        try ensureUtf8(body, "request body");

        var response_body: std.Io.Writer.Allocating = .init(self.allocator);
        defer response_body.deinit();

        var headers_buf: [4]std.http.Header = undefined;
        var headers_len: usize = 0;
        headers_buf[headers_len] = .{ .name = "Content-Type", .value = "application/json; charset=utf-8" };
        headers_len += 1;
        headers_buf[headers_len] = .{ .name = "Accept", .value = "application/json, text/event-stream" };
        headers_len += 1;
        headers_buf[headers_len] = .{ .name = "MCP-Protocol-Version", .value = protocol_version };
        headers_len += 1;
        if (self.session_id) |sid| {
            headers_buf[headers_len] = .{ .name = "Mcp-Session-Id", .value = sid };
            headers_len += 1;
        }

        var http_client: std.http.Client = .{ .allocator = self.allocator, .io = self.io };
        defer http_client.deinit();

        const result = try http_client.fetch(.{
            .location = .{ .url = self.endpoint_url },
            .method = .POST,
            .payload = body,
            .response_writer = &response_body.writer,
            .keep_alive = false,
            .extra_headers = headers_buf[0..headers_len],
        });

        const status_code: u16 = @intFromEnum(result.status);
        if (status_code == 202 and !want_body) {
            return self.allocator.dupe(u8, "");
        }
        if (status_code < 200 or status_code >= 300) {
            const error_body = response_body.written();
            if (std.unicode.utf8ValidateSlice(error_body)) {
                std.debug.print("HTTP {d}: {s}\n", .{ status_code, std.mem.trim(u8, error_body, " \t\r\n") });
            } else {
                std.debug.print("HTTP {d}: response body is not valid UTF-8\n", .{status_code});
            }
            return AppError.HTTPStatusError;
        }
        if (!want_body) return self.allocator.dupe(u8, "");

        try ensureUtf8(response_body.written(), "response body");

        const trimmed = std.mem.trim(u8, response_body.written(), " \t\r\n");
        if (std.mem.startsWith(u8, trimmed, "data:")) {
            return extractSSEData(self.allocator, response_body.written());
        }
        return self.allocator.dupe(u8, response_body.written());
    }

    fn extractResult(self: *Client, response_json: []u8) ![]u8 {
        var parsed = try std.json.parseFromSlice(std.json.Value, self.allocator, response_json, .{});
        defer parsed.deinit();

        const obj = parsed.value.object;
        if (obj.get("error")) |err_value| {
            try printRPCError(err_value);
            return AppError.HTTPStatusError;
        }

        const result = obj.get("result") orelse return AppError.MissingJSONRPCResult;
        var out: std.Io.Writer.Allocating = .init(self.allocator);
        defer out.deinit();
        try out.writer.print("{f}", .{std.json.fmt(result, .{})});
        return try self.allocator.dupe(u8, out.written());
    }
};

pub fn main(init: std.process.Init) !u8 {
    setWindowsConsoleUtf8();

    run(init.gpa, init.arena.allocator(), init.io, init.minimal.args, init.environ_map) catch |err| {
        if (err != AppError.Usage and err != AppError.HTTPStatusError and err != AppError.ToolNotFound) {
            std.debug.print("error: {s}\n", .{@errorName(err)});
        }
        return 1;
    };
    return 0;
}

fn run(allocator: std.mem.Allocator, arena: std.mem.Allocator, io: std.Io, process_args: std.process.Args, environ_map: *std.process.Environ.Map) !void {
    var stdout_buffer: [4096]u8 = undefined;
    var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);
    const stdout = &stdout_writer.interface;
    defer stdout.flush() catch {};

    const argv = try argsToSlice(process_args, allocator, arena);

    var flag_url: ?[]const u8 = null;
    var command_index: usize = 1;
    while (command_index < argv.len) : (command_index += 1) {
        const arg = argv[command_index];
        if (std.mem.eql(u8, arg, "-url")) {
            command_index += 1;
            if (command_index >= argv.len) return usage("missing value for -url");
            flag_url = argv[command_index];
            continue;
        }
        break;
    }

    if (command_index >= argv.len) return usage("missing command");

    const endpoint_url = try resolveEndpointURL(allocator, flag_url, environ_map);
    defer allocator.free(endpoint_url);

    var client = Client{ .allocator = allocator, .io = io, .endpoint_url = endpoint_url };
    defer client.deinit();
    try client.initialize();

    const command = argv[command_index];
    if (std.mem.eql(u8, command, "list_tools")) {
        if (command_index + 1 != argv.len) return usage("list_tools takes no arguments");
        const tools_json = try client.listTools();
        defer allocator.free(tools_json);
        try printToolSummaries(allocator, stdout, tools_json);
    } else if (std.mem.eql(u8, command, "get_tool_schema")) {
        if (command_index + 2 != argv.len) return usage("get_tool_schema takes exactly one argument: tool name");
        const tools_json = try client.listTools();
        defer allocator.free(tools_json);
        try printToolSchema(allocator, stdout, tools_json, argv[command_index + 1]);
    } else if (std.mem.eql(u8, command, "call_tool")) {
        if (command_index + 3 != argv.len) return usage("call_tool takes exactly two arguments: tool name and JSON arguments");
        try ensureUtf8(argv[command_index + 2], "call_tool JSON arguments");
        var parsed_args = try std.json.parseFromSlice(std.json.Value, allocator, argv[command_index + 2], .{});
        parsed_args.deinit();

        const result = try client.callTool(argv[command_index + 1], argv[command_index + 2]);
        defer allocator.free(result);
        try printPrettyJSON(allocator, stdout, result);
    } else {
        return usage("unknown command");
    }
}

fn setWindowsConsoleUtf8() void {
    if (builtin.os.tag != .windows) return;

    const windows = std.os.windows;
    const console = struct {
        extern "kernel32" fn SetConsoleOutputCP(wCodePageID: u32) windows.BOOL;
        extern "kernel32" fn SetConsoleCP(wCodePageID: u32) windows.BOOL;
    };
    const cp_utf8 = 65001;
    _ = console.SetConsoleOutputCP(cp_utf8);
    _ = console.SetConsoleCP(cp_utf8);
}

fn argsToSlice(process_args: std.process.Args, temp_allocator: std.mem.Allocator, arena: std.mem.Allocator) ![]const []const u8 {
    var it = try process_args.iterateAllocator(temp_allocator);
    defer it.deinit();

    var argv = std.ArrayList([]const u8).empty;
    while (it.next()) |arg| {
        const copied = try arena.dupe(u8, arg);
        try argv.append(arena, copied);
    }
    return argv.toOwnedSlice(arena);
}

fn ensureUtf8(bytes: []const u8, what: []const u8) AppError!void {
    if (!std.unicode.utf8ValidateSlice(bytes)) {
        std.debug.print("error: {s} is not valid UTF-8\n", .{what});
        return AppError.InvalidUTF8;
    }
}

fn usage(message: []const u8) AppError!void {
    std.debug.print(
        "error: {s}\n\nusage:\n  mcp-tool-zig [-url http://host/mcp] list_tools\n  mcp-tool-zig [-url http://host/mcp] get_tool_schema <tool_name>\n  mcp-tool-zig [-url http://host/mcp] call_tool <tool_name> '<json_arguments>'\n",
        .{message},
    );
    return AppError.Usage;
}

fn resolveEndpointURL(allocator: std.mem.Allocator, flag_url: ?[]const u8, environ_map: *std.process.Environ.Map) ![]u8 {
    if (flag_url) |url| {
        const trimmed = std.mem.trim(u8, url, " \t\r\n");
        if (trimmed.len != 0) return allocator.dupe(u8, trimmed);
    }
    if (environ_map.get("MCP_URL")) |url| {
        const trimmed = std.mem.trim(u8, url, " \t\r\n");
        if (trimmed.len != 0) return allocator.dupe(u8, trimmed);
    }

    if (try readEndpointURLFromRegistry(allocator)) |url| return url;
    return allocator.dupe(u8, default_endpoint_url);
}

fn readEndpointURLFromRegistry(allocator: std.mem.Allocator) !?[]u8 {
    if (builtin.os.tag != .windows) return null;

    const keys = [_][]const u8{
        "HKCU\\Software\\XXXX\\MCP",
        "HKLM\\Software\\XXXX\\MCP",
    };
    const values = [_][]const u8{ "URL", "MCP_URL" };

    for (keys) |key| {
        for (values) |value| {
            if (try queryRegistryString(allocator, key, value)) |url| return url;
        }
    }
    return null;
}

fn queryRegistryString(allocator: std.mem.Allocator, key: []const u8, value: []const u8) !?[]u8 {
    if (builtin.os.tag != .windows) return null;

    const windows = std.os.windows;
    const reg = struct {
        extern "advapi32" fn RegOpenKeyExW(
            hKey: windows.HKEY,
            lpSubKey: [*:0]const u16,
            ulOptions: windows.DWORD,
            samDesired: windows.REGSAM,
            phkResult: *windows.HKEY,
        ) windows.LSTATUS;

        extern "advapi32" fn RegQueryValueExW(
            hKey: windows.HKEY,
            lpValueName: [*:0]const u16,
            lpReserved: ?*windows.DWORD,
            lpType: ?*windows.DWORD,
            lpData: ?[*]u8,
            lpcbData: *windows.DWORD,
        ) windows.LSTATUS;

        extern "advapi32" fn RegCloseKey(hKey: windows.HKEY) windows.LSTATUS;
    };

    const slash = std.mem.indexOf(u8, key, "\\") orelse return null;
    const root_name = key[0..slash];
    const sub_key = key[slash + 1 ..];
    const root: windows.HKEY = if (std.mem.eql(u8, root_name, "HKCU"))
        windows.HKEY_CURRENT_USER
    else if (std.mem.eql(u8, root_name, "HKLM"))
        windows.HKEY_LOCAL_MACHINE
    else
        return null;

    const sub_key_w = try std.unicode.utf8ToUtf16LeAllocZ(allocator, sub_key);
    defer allocator.free(sub_key_w);
    const value_w = try std.unicode.utf8ToUtf16LeAllocZ(allocator, value);
    defer allocator.free(value_w);

    var opened: windows.HKEY = undefined;
    const key_query_value: windows.REGSAM = .{ .SPECIFIC = .{ .KEY = .{ .QUERY_VALUE = true } } };
    const open_status = reg.RegOpenKeyExW(root, sub_key_w, 0, key_query_value, &opened);
    if (open_status != 0) return null;
    defer _ = reg.RegCloseKey(opened);

    var value_type: windows.REG.ValueType = .NONE;
    var byte_len: windows.DWORD = 0;
    const size_status = reg.RegQueryValueExW(opened, value_w, null, @ptrCast(&value_type), null, &byte_len);
    if (size_status != 0 or byte_len == 0) return null;
    if (value_type != .SZ and value_type != .EXPAND_SZ) return null;

    const u16_capacity = (byte_len + 1) / 2;
    const data = try allocator.alloc(u16, u16_capacity);
    defer allocator.free(data);
    const query_status = reg.RegQueryValueExW(opened, value_w, null, @ptrCast(&value_type), @ptrCast(data.ptr), &byte_len);
    if (query_status != 0 or byte_len < 2) return null;

    const u16_len = byte_len / 2;
    const wide = data[0..u16_len];
    const text_w = if (wide.len > 0 and wide[wide.len - 1] == 0) wide[0 .. wide.len - 1] else wide;
    const text = try std.unicode.utf16LeToUtf8Alloc(allocator, text_w);
    const trimmed = std.mem.trim(u8, text, " \t\r\n");
    if (trimmed.len == text.len) return text;
    defer allocator.free(text);
    if (trimmed.len == 0) return null;
    const copied = try allocator.dupe(u8, trimmed);
    return copied;
}

fn parseEndpoint(url: []const u8) !Endpoint {
    const prefix = "http://";
    if (!std.mem.startsWith(u8, url, prefix)) return AppError.UnsupportedURLScheme;

    const rest = url[prefix.len..];
    const slash = std.mem.indexOfScalar(u8, rest, '/') orelse rest.len;
    const host_port = rest[0..slash];
    const path = if (slash < rest.len) rest[slash..] else "/";
    if (host_port.len == 0) return AppError.InvalidURL;

    if (std.mem.lastIndexOfScalar(u8, host_port, ':')) |colon| {
        const host = host_port[0..colon];
        const port = try std.fmt.parseInt(u16, host_port[colon + 1 ..], 10);
        if (host.len == 0) return AppError.InvalidURL;
        return .{ .host = host, .port = port, .path = path };
    }
    return .{ .host = host_port, .port = 80, .path = path };
}

fn parseHTTPResponse(raw: []const u8) !HTTPResponse {
    const split = std.mem.indexOf(u8, raw, "\r\n\r\n") orelse return AppError.InvalidHTTPResponse;
    const head = raw[0..split];
    const body_start = split + 4;
    var lines = std.mem.splitSequence(u8, head, "\r\n");
    const status_line = lines.next() orelse return AppError.InvalidHTTPResponse;
    var parts = std.mem.tokenizeScalar(u8, status_line, ' ');
    _ = parts.next() orelse return AppError.InvalidHTTPResponse;
    const status_text = parts.next() orelse return AppError.InvalidHTTPResponse;
    const status = try std.fmt.parseInt(u16, status_text, 10);
    return .{ .status = status, .headers = head, .body = raw[body_start..] };
}

fn getHeaderValue(headers: []const u8, name: []const u8) ?[]const u8 {
    var lines = std.mem.splitSequence(u8, headers, "\r\n");
    _ = lines.next();
    while (lines.next()) |line| {
        const colon = std.mem.indexOfScalar(u8, line, ':') orelse continue;
        const key = std.mem.trim(u8, line[0..colon], " \t");
        if (std.ascii.eqlIgnoreCase(key, name)) {
            return std.mem.trim(u8, line[colon + 1 ..], " \t\r\n");
        }
    }
    return null;
}

fn containsIgnoreCase(haystack: []const u8, needle: []const u8) bool {
    if (needle.len == 0) return true;
    if (haystack.len < needle.len) return false;

    var i: usize = 0;
    while (i + needle.len <= haystack.len) : (i += 1) {
        if (std.ascii.eqlIgnoreCase(haystack[i .. i + needle.len], needle)) return true;
    }
    return false;
}

fn extractSSEData(allocator: std.mem.Allocator, body: []const u8) ![]u8 {
    var out = std.ArrayList(u8).empty;
    errdefer out.deinit(allocator);

    var lines = std.mem.splitScalar(u8, body, '\n');
    while (lines.next()) |raw_line| {
        const line = std.mem.trim(u8, raw_line, "\r");
        if (line.len == 0) {
            if (out.items.len != 0) break;
            continue;
        }
        if (std.mem.startsWith(u8, line, "data:")) {
            try out.appendSlice(allocator, std.mem.trim(u8, line[5..], " \t"));
        }
    }
    if (out.items.len == 0) return AppError.EmptySSEResponse;
    return out.toOwnedSlice(allocator);
}

fn decodeChunkedBody(allocator: std.mem.Allocator, body: []const u8) ![]u8 {
    var out = std.ArrayList(u8).empty;
    errdefer out.deinit(allocator);

    var pos: usize = 0;
    while (true) {
        const line_end_rel = std.mem.indexOf(u8, body[pos..], "\r\n") orelse return AppError.InvalidHTTPResponse;
        const line_end = pos + line_end_rel;
        const size_line = body[pos..line_end];
        const semicolon = std.mem.indexOfScalar(u8, size_line, ';') orelse size_line.len;
        const size_text = std.mem.trim(u8, size_line[0..semicolon], " \t");
        const size = try std.fmt.parseInt(usize, size_text, 16);

        pos = line_end + 2;
        if (size == 0) break;
        if (pos + size + 2 > body.len) return AppError.InvalidHTTPResponse;
        try out.appendSlice(allocator, body[pos .. pos + size]);
        pos += size;
        if (!std.mem.eql(u8, body[pos .. pos + 2], "\r\n")) return AppError.InvalidHTTPResponse;
        pos += 2;
    }

    return out.toOwnedSlice(allocator);
}

fn printToolSummaries(allocator: std.mem.Allocator, stdout: anytype, tools_json: []const u8) !void {
    var parsed = try std.json.parseFromSlice(std.json.Value, allocator, tools_json, .{});
    defer parsed.deinit();

    const tools = parsed.value.object.get("tools") orelse return;
    for (tools.array.items) |item| {
        const obj = item.object;
        const name = obj.get("name") orelse continue;
        const desc = obj.get("description");
        if (desc) |d| {
            try stdout.print("{s}\t{s}\n", .{ name.string, d.string });
        } else {
            try stdout.print("{s}\n", .{name.string});
        }
    }
}

fn printToolSchema(allocator: std.mem.Allocator, stdout: anytype, tools_json: []const u8, wanted_name: []const u8) !void {
    var parsed = try std.json.parseFromSlice(std.json.Value, allocator, tools_json, .{});
    defer parsed.deinit();

    const tools = parsed.value.object.get("tools") orelse return AppError.ToolNotFound;
    for (tools.array.items) |item| {
        const obj = item.object;
        const name = obj.get("name") orelse continue;
        if (!std.mem.eql(u8, name.string, wanted_name)) continue;
        try writePrettyJSONValue(allocator, stdout, item);
        return;
    }

    std.debug.print("error: tool \"{s}\" not found\n", .{wanted_name});
    return AppError.ToolNotFound;
}

fn printPrettyJSON(allocator: std.mem.Allocator, stdout: anytype, raw: []const u8) !void {
    var parsed = try std.json.parseFromSlice(std.json.Value, allocator, raw, .{});
    defer parsed.deinit();

    try writePrettyJSONValue(allocator, stdout, parsed.value);
}

fn writePrettyJSONValue(allocator: std.mem.Allocator, stdout: anytype, value: std.json.Value) !void {
    var out: std.Io.Writer.Allocating = .init(allocator);
    defer out.deinit();

    var json_writer: std.json.Stringify = .{
        .writer = &out.writer,
        .options = .{ .whitespace = .indent_2 },
    };
    try json_writer.write(value);
    try stdout.print("{s}\n", .{out.written()});
}

fn printRPCError(value: std.json.Value) !void {
    const obj = value.object;
    const code = if (obj.get("code")) |v| v.integer else 0;
    const message = if (obj.get("message")) |v| v.string else "unknown JSON-RPC error";
    std.debug.print("error: JSON-RPC error {d}: {s}\n", .{ code, message });
}

MCP 服务地址解析规则

MCP endpoint URL 按以下优先级解析:

  1. 命令行参数:-url http://host/mcp
  2. 环境变量:MCP_URL
  3. Windows 注册表:
    • HKCU\Software\XXXX\MCP 下的 URLMCP_URL
    • HKLM\Software\XXXX\MCP 下的 URLMCP_URL
  4. 默认地址:
http://127.0.0.1:8080/mcp

UTF-8 处理

MCP 请求体使用 UTF-8 编码的 JSON,并设置请求头:

Content-Type: application/json; charset=utf-8

程序会进行以下 UTF-8 校验:

  • call_tool 的 JSON 参数在解析前必须是合法 UTF-8。
  • JSON-RPC 请求体在发送前必须是合法 UTF-8。
  • MCP 响应体在 JSON 解析或输出前必须是合法 UTF-8。

在 Windows 上,程序启动时会将控制台输入和输出代码页设置为 UTF-8,也就是 65001,尽量避免中文输出乱码。

编译步骤

当前验证使用的 Zig 编译器路径为:

%USERPROFILE%\AppData\Local\Microsoft\WinGet\Packages\zig.zig_Microsoft.Winget.Source_8wekyb3d8bbwe\zig-x86_64-windows-0.16.0\zig.exe

在项目根目录执行:

%USERPROFILE%\AppData\Local\Microsoft\WinGet\Packages\zig.zig_Microsoft.Winget.Source_8wekyb3d8bbwe\zig-x86_64-windows-0.16.0\zig.exe build

编译后的可执行文件位置:

zig-out\bin\mcp-tool-zig.exe

当前优化后的可执行文件体积约为 606 KB。

使用说明

列出工具

使用默认服务地址解析规则:

.\zig-out\bin\mcp-tool-zig.exe list_tools

显式指定 MCP 服务地址:

.\zig-out\bin\mcp-tool-zig.exe -url http://127.0.0.1:8080/mcp list_tools

输出格式:

工具名称<TAB>工具描述

获取工具 Schema

.\zig-out\bin\mcp-tool-zig.exe get_tool_schema tool-name

输出为格式化 JSON,例如:

{
  "name": "tool-name",
  "description": "...",
  "inputSchema": {},
  "outputSchema": {}
}

调用工具

.\zig-out\bin\mcp-tool-zig.exe call_tool <tool_name> '<json_arguments>'

示例:

.\zig-out\bin\mcp-tool-zig.exe call_tool platform-lidar360_about_info '{}'

在 Windows PowerShell 中,也可以这样传递 JSON 参数:

.\zig-out\bin\mcp-tool-zig.exe call_tool platform-lidar360_about_info "{}"

工具调用结果会以格式化 JSON 输出。

错误处理

所有错误信息都会输出到 stderr,并且程序会以非 0 退出码退出。

常见错误包括:

  • 未知命令
  • 缺少命令参数
  • JSON 参数格式错误
  • 输入或响应不是合法 UTF-8
  • HTTP 返回非 2xx 状态码
  • MCP 服务返回 JSON-RPC error
  • 指定的工具不存在

当前限制

  • 仅支持 http:// endpoint,HTTPS/TLS 已被明确禁用。
  • 当前实现使用 std.http.Client.fetch,该高级接口不暴露响应头。因此程序不会从服务端响应头中读取新的 Mcp-Session-Id
  • 该工具面向普通命令行调用和小体积二进制文件,不面向高并发场景。
posted @ 2026-07-07 10:51  乌合之众  阅读(13)  评论(0)    收藏  举报
clear