Something went wrong. Try again.
a Model Context Protocol library for zig, server and client
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172//! An MCP server on stdio exposing one of each primitive. Run it with//! `zig build example`, or point an MCP client at `zig-out/bin/mcp-example`.
const std = @import("std");const mcp = @import("mcp");const schema = mcp.schema;
const Handler = struct { const tools = [_]schema.Tool{ .{ .name = "echo", .description = "Repeat the given text.", .inputSchema = .{ .raw = \\{ \\ "type": "object", \\ "properties": { "text": { "type": "string" } }, \\ "required": ["text"] \\} }, .annotations = .{ .readOnlyHint = true }, }, .{ .name = "add", .description = "Add two integers.", .inputSchema = .{ .raw = \\{ \\ "type": "object", \\ "properties": { "a": { "type": "integer" }, "b": { "type": "integer" } }, \\ "required": ["a", "b"] \\} }, .outputSchema = .{ .raw = \\{ "type": "object", "properties": { "sum": { "type": "integer" } }, "required": ["sum"] } }, }, .{ .name = "confirm", .description = "Ask the user to confirm, then report the answer.", .inputSchema = .{ .raw = \\{ "type": "object", "properties": { "question": { "type": "string" } }, "required": ["question"] } }, }, };
pub fn listTools(_: *Handler, _: *mcp.Request, _: schema.ListParams) !schema.ListToolsResult { return .{ .tools = &tools, .ttlMs = 60_000, .cacheScope = .public }; }
pub fn callTool(_: *Handler, req: *mcp.Request, params: schema.CallToolParams) !schema.Outcome(schema.CallToolResult) { if (std.mem.eql(u8, params.name, "confirm")) return confirm(req, params); return .{ .complete = try callSimpleTool(req, params) }; }
/// Multi round-trip request: the first call returns an elicitation for /// the client to put to the user; the retry carries the answer. fn confirm(req: *mcp.Request, params: schema.CallToolParams) !schema.Outcome(schema.CallToolResult) { try req.requireClientCapability("elicitation");
const responses = params.inputResponses orelse { const question = if (params.arguments) |a| switch (a) { .object => |o| if (o.get("question")) |q| switch (q) { .string => |s| s, else => null, } else null, else => null, } else null; const message = question orelse return req.reject(.invalid_params, "missing question"); const requests = try std.json.Stringify.valueAlloc(req.arena, .{ .confirm = .{ .method = "elicitation/create", .params = .{ .mode = "form", .message = message, // `.{}` would be an empty tuple, which serializes as `[]`. .requestedSchema = .{ .type = "object", .properties = struct {}{} }, }, }, }, .{}); return .{ .input_required = .{ .inputRequests = .{ .raw = requests } } }; };
const action = blk: { if (responses != .object) break :blk null; const answer = responses.object.get("confirm") orelse break :blk null; if (answer != .object) break :blk null; const a = answer.object.get("action") orelse break :blk null; break :blk if (a == .string) a.string else null; } orelse return req.reject(.invalid_params, "missing confirm response"); const text = try std.fmt.allocPrint(req.arena, "user chose {s}", .{action}); const content = try req.arena.dupe(schema.Content, &.{.fromText(text)}); return .{ .complete = .{ .content = content } }; }
fn callSimpleTool(req: *mcp.Request, params: schema.CallToolParams) !schema.CallToolResult { const args = if (params.arguments) |a| switch (a) { .object => |o| o, else => return req.reject(.invalid_params, "arguments must be an object"), } else return req.reject(.invalid_params, "arguments are required");
if (std.mem.eql(u8, params.name, "echo")) { const text = args.get("text") orelse return req.reject(.invalid_params, "missing text"); if (text != .string) return req.reject(.invalid_params, "text must be a string"); const content = try req.arena.dupe(schema.Content, &.{.fromText(text.string)}); return .{ .content = content }; } if (std.mem.eql(u8, params.name, "add")) { const a = args.get("a") orelse return req.reject(.invalid_params, "missing a"); const b = args.get("b") orelse return req.reject(.invalid_params, "missing b"); if (a != .integer or b != .integer) return req.reject(.invalid_params, "a and b must be integers"); const sum = std.math.add(i64, a.integer, b.integer) catch { // The tool ran and failed: report it to the model, not as a // protocol error. const content = try req.arena.dupe(schema.Content, &.{.fromText("integer overflow")}); return .{ .content = content, .isError = true }; }; const text = try std.fmt.allocPrint(req.arena, "{d}", .{sum}); const structured = try std.fmt.allocPrint(req.arena, "{{\"sum\":{d}}}", .{sum}); const content = try req.arena.dupe(schema.Content, &.{.fromText(text)}); return .{ .content = content, .structuredContent = .{ .raw = structured } }; } return req.reject(.invalid_params, "Unknown tool"); }
const readme_uri = "memo://readme";
pub fn listResources(_: *Handler, _: *mcp.Request, _: schema.ListParams) !schema.ListResourcesResult { return .{ .resources = &.{.{ .uri = readme_uri, .name = "readme", .mimeType = "text/plain" }}, .cacheScope = .public }; }
pub fn readResource(_: *Handler, req: *mcp.Request, params: schema.ReadResourceParams) !schema.ReadResourceResult { if (!std.mem.eql(u8, params.uri, readme_uri)) return req.resourceNotFound(params.uri); return .{ .contents = &.{.{ .text = .{ .uri = readme_uri, .mimeType = "text/plain", .text = "a slim MCP server written in zig" } }}, .cacheScope = .public, }; }
pub fn listPrompts(_: *Handler, _: *mcp.Request, _: schema.ListParams) !schema.ListPromptsResult { return .{ .prompts = &.{.{ .name = "greet", .description = "Greet someone by name.", .arguments = &.{.{ .name = "name", .required = true }}, }}, .cacheScope = .public }; }
pub fn getPrompt(_: *Handler, req: *mcp.Request, params: schema.GetPromptParams) !schema.GetPromptResult { if (!std.mem.eql(u8, params.name, "greet")) return req.reject(.invalid_params, "Unknown prompt"); const name = if (params.arguments) |a| switch (a) { .object => |o| o.get("name"), else => null, } else null; if (name == null or name.? != .string) return req.reject(.invalid_params, "Missing required argument: name"); const text = try std.fmt.allocPrint(req.arena, "say hello to {s}", .{name.?.string}); const messages = try req.arena.dupe(schema.PromptMessage, &.{.{ .role = .user, .content = .fromText(text) }}); return .{ .messages = messages }; }};
pub fn main(init: std.process.Init) !void { var handler: Handler = .{}; var server: mcp.Server(Handler) = try .init(init.io, init.gpa, &handler, .{ .info = .{ .name = "mcp-example", .version = "0.0.1" }, .instructions = "Tools: echo, add, confirm. Resource: memo://readme. Prompt: greet.", });
var in_buf: [64 * 1024]u8 = undefined; var out_buf: [64 * 1024]u8 = undefined; var stdin = std.Io.File.stdin().readerStreaming(init.io, &in_buf); var stdout = std.Io.File.stdout().writerStreaming(init.io, &out_buf); try mcp.stdio.serve(&server, init.io, init.gpa, &stdin.interface, &stdout.interface, .{});}