diff --git a/.devwork/plan.md b/.devwork/plan.md new file mode 100644 index 0000000..9f62249 --- /dev/null +++ b/.devwork/plan.md @@ -0,0 +1,6 @@ +# Execution Plan + +## Tasks: 7 + +{"summary":"Implement MCP (Model Context Protocol) support for gRPC-zig, a JSON-RPC 2.0 based protocol for AI assistants. This involves creating MCP types, protocol handling, transport layer (stdio/HTTP+SSE), and server implementation compatible with Claude.","tasks":[{"id":"T001","tool":"Bash","args":{"command":"mkdir -p /tmp/claudev-pr-95466/src/mcp"},"depends_on":[],"description":"Create MCP directory structure"},{"id":"T002","tool":"Write","args":{"file_path":"/tmp/claudev-pr-95466/src/mcp/types.zig","content":"const std = @import(\"std\");\n\npub const JSONRPCVersion = \"2.0\";\n\npub const ErrorCode = enum(i32) {\n ParseError = -32700,\n InvalidRequest = -32600,\n MethodNotFound = -32601,\n InvalidParams = -32602,\n InternalError = -32603,\n};\n\npub const JSONRPCError = struct {\n code: ErrorCode,\n message: []const u8,\n data: ?[]const u8 = null,\n};\n\npub const ProgressToken = union(enum) {\n string: []const u8,\n number: i64,\n};\n\npub const JSONValue = union(enum) {\n null,\n bool: bool,\n number: f64,\n string: []const u8,\n array: []JSONValue,\n object: std.json.ObjectMap,\n};\n\npub const TextContent = struct {\n type: []const u8 = \"text\",\n text: []const u8,\n};\n\npub const ImageContent = struct {\n type: []const u8 = \"image\",\n data: []const u8,\n mimeType: []const u8,\n};\n\npub const BlobResourceContents = struct {\n type: []const u8 = \"blob\",\n blob: []const u8,\n mimeType: []const u8,\n};\n\npub const EmbeddedResource = struct {\n type: []const u8 = \"resource\",\n resource: Resource,\n};\n\npub const Content = union(enum) {\n text: TextContent,\n image: ImageContent,\n blob: BlobResourceContents,\n embedded: EmbeddedResource,\n};\n\npub const Resource = struct {\n uri: []const u8,\n mimeType: []const u8,\n text: ?[]const u8 = null,\n blob: ?[]const u8 = null,\n};\n\npub const TextResourceContents = struct {\n uri: []const u8,\n mimeType: []const u8,\n text: []const u8,\n};\n\npub const CallToolResult = struct {\n content: []Content,\n isError: bool = false,\n};\n\npub const Tool = struct {\n name: []const u8,\n description: []const u8,\n inputSchema: std.json.ObjectMap,\n};\n\npub const ResourceTemplate = struct {\n uriTemplate: []const u8,\n name: []const u8,\n description: []const u8,\n mimeType: []const u8,\n};\n\npub const Resource_ = struct {\n uri: []const u8,\n name: []const u8,\n description: []const u8,\n mimeType: []const u8,\n};\n\npub const Prompt = struct {\n name: []const u8,\n description: []const u8,\n arguments: ?[]struct {\n name: []const u8,\n description: []const u8,\n required: bool,\n } = null,\n};\n\npub const PromptMessage = struct {\n role: []const u8,\n content: Content,\n};\n\npub const PromptResult = struct {\n messages: []PromptMessage,\n};\n\npub const SamplingMessage = struct {\n role: []const u8,\n content: Content,\n};\n\npub const CreateMessageRequest = struct {\n messages: []SamplingMessage,\n systemPrompt: ?[]const u8 = null,\n includeContext: []const u8,\n temperature: ?f64 = null,\n maxTokens: ?u32 = null,\n};\n\npub const CreateMessageResult = struct {\n role: []const u8,\n content: Content,\n model: []const u8,\n stopReason: []const u8,\n};\n\npub const InitializeResult = struct {\n protocolVersion: []const u8,\n capabilities: ServerCapabilities,\n serverInfo: ServerInfo,\n};\n\npub const ServerCapabilities = struct {\n tools: ?struct {\n listChanged: bool = false,\n } = null,\n resources: ?struct {\n subscribe: bool = false,\n listChanged: bool = false,\n } = null,\n prompts: ?struct {\n listChanged: bool = false,\n } = null,\n sampling: ?struct {},\n};\n\npub const ServerInfo = struct {\n name: []const u8,\n version: []const u8,\n};\n\npub const ClientCapabilities = struct {\n tools: ?struct {\n listChanged: bool = false,\n } = null,\n resources: ?struct {\n subscribe: bool = false,\n listChanged: bool = false,\n } = null,\n prompts: ?struct {\n listChanged: bool = false,\n } = null,\n sampling: ?struct {},\n};\n\npub const InitializeParams = struct {\n protocolVersion: []const u8,\n capabilities: ClientCapabilities,\n clientInfo: ClientInfo,\n};\n\npub const ClientInfo = struct {\n name: []const u8,\n version: []const u8,\n};\n\npub const EmptyParams = struct {};\n\npub const ListToolsResult = struct {\n tools: []Tool,\n};\n\npub const ListResourcesResult = struct {\n resources: []Resource_,\n};\n\npub const ListResourceTemplatesResult = struct {\n resourceTemplates: []ResourceTemplate,\n};\n\npub const ListPromptsResult = struct {\n prompts: []Prompt,\n};\n\npub const ReadResourceResult = struct {\n contents: []const union(enum) {\n TextResourceContents,\n BlobResourceContents,\n },\n};\n\npub const JSONRPCMessage = union(enum) {\n request: JSONRPCRequest,\n notification: JSONRPCNotification,\n response: JSONRPCResponse,\n error: JSONRPCErrorResponse,\n};\n\npub const JSONRPCRequest = struct {\n jsonrpc: []const u8,\n id: union(enum) {\n string: []const u8,\n number: i64,\n },\n method: []const u8,\n params: ?JSONValue = null,\n};\n\npub const JSONRPCNotification = struct {\n jsonrpc: []const u8,\n method: []const u8,\n params: ?JSONValue = null,\n};\n\npub const JSONRPCResponse = struct {\n jsonrpc: []const u8,\n id: union(enum) {\n string: []const u8,\n number: i64,\n },\n result: JSONValue,\n};\n\npub const JSONRPCErrorResponse = struct {\n jsonrpc: []const u8,\n id: ?union(enum) {\n string: []const u8,\n number: i64,\n } = null,\n error: JSONRPCError,\n};\n\npub fn parseMessage(allocator: std.mem.Allocator, data: []const u8) !JSONRPCMessage {\n const parsed = try std.json.parseFromSlice(JSONValue, allocator, data, .{});\n defer parsed.deinit();\n\n const value = parsed.value;\n if (value != .object) {\n return error.InvalidJSON;\n }\n\n const obj = value.object;\n\n // Check if it's a request (has method and id)\n if (obj.get(\"method\")) |method_val| {\n if (obj.get(\"id\")) |id_val| {\n // Request\n const method = method_val.string;\n const id = if (id_val == .number) .{ number = @intCast(id_val.number) } else .{ string = id_val.string };\n const params = if (obj.get(\"params\")) |p| p else null;\n\n return JSONRPCMessage{\n .request = .{\n .jsonrpc = JSONRPCVersion,\n .id = id,\n .method = method,\n .params = params,\n },\n };\n } else {\n // Notification\n const method = method_val.string;\n const params = if (obj.get(\"params\")) |p| p else null;\n\n return JSONRPCMessage{\n .notification = .{\n .jsonrpc = JSONRPCVersion,\n .method = method,\n .params = params,\n },\n };\n }\n }\n\n // Check if it's an error response\n if (obj.get(\"error\")) |_| {\n const id = if (obj.get(\"id\")) |id_val| if (id_val == .number) .{ number = @intCast(id_val.number) } else .{ string = id_val.string } else null;\n const err_obj = obj.get(\"error\").?.object;\n const code = @enumFromInt(@intFromFloat(err_obj.get(\"code\").?.number));\n const message = err_obj.get(\"message\").?.string;\n const data_val = if (err_obj.get(\"data\")) |d| d.string else null;\n\n return JSONRPCMessage{\n .error = .{\n .jsonrpc = JSONRPCVersion,\n .id = id,\n .error = .{ .code = code, .message = message, .data = data_val },\n },\n };\n }\n\n // Must be a response\n const id = if (obj.get(\"id\").?) |id_val| if (id_val == .number) .{ number = @intCast(id_val.number) } else .{ string = id_val.string } else unreachable;\n const result = obj.get(\"result\").?;\n\n return JSONRPCMessage{\n .response = .{\n .jsonrpc = JSONRPCVersion,\n .id = id,\n .result = result,\n },\n };\n}\n\npub fn serializeMessage(allocator: std.mem.Allocator, msg: JSONRPCMessage) ![]u8 {\n var buf = std.ArrayList(u8).init(allocator);\n try serializeMessageWriter(msg, buf.writer());\n return buf.toOwnedSlice();\n}\n\nfn serializeMessageWriter(msg: JSONRPCMessage, writer: anytype) !void {\n switch (msg) {\n .request => |req| {\n try writer.writeByte('{');\n try writer.print(\"\\\"jsonrpc\\\":\\\"{s}\\\",\\\"id\\\":\", .{req.jsonrpc});\n switch (req.id) {\n .string => |s| try writer.print(\"\\\"{s}\\\",\", .{s}),\n .number => |n| try writer.print(\"{},\", .{n}),\n }\n try writer.print(\"\\\"method\\\":\\\"{s}\\\"\", .{req.method});\n if (req.params) |params| {\n try writer.writeByte(',');\n try writer.writeAll(\"\\\"params\\\":\");\n try jsonValueWrite(params, writer);\n }\n try writer.writeByte('}');\n },\n .notification => |notif| {\n try writer.writeByte('{');\n try writer.print(\"\\\"jsonrpc\\\":\\\"{s}\\\",\\\"method\\\":\\\"{s}\\\"\", .{notif.jsonrpc, notif.method});\n if (notif.params) |params| {\n try writer.writeByte(',');\n try writer.writeAll(\"\\\"params\\\":\");\n try jsonValueWrite(params, writer);\n }\n try writer.writeByte('}');\n },\n .response => |resp| {\n try writer.writeByte('{');\n try writer.print(\"\\\"jsonrpc\\\":\\\"{s}\\\",\\\"id\\\":\", .{resp.jsonrpc});\n switch (resp.id) {\n .string => |s| try writer.print(\"\\\"{s}\\\",\", .{s}),\n .number => |n| try writer.print(\"{},\", .{n}),\n }\n try writer.writeAll(\"\\\"result\\\":\");\n try jsonValueWrite(resp.result, writer);\n try writer.writeByte('}');\n },\n .error => |err| {\n try writer.writeByte('{');\n try writer.print(\"\\\"jsonrpc\\\":\\\"{s}\\\",\\\"error\\\":{{\"code\":{},\\\"message\\\":\\\"{s}\\\"\", .{err.jsonrpc, @intFromEnum(err.error.code), err.error.message});\n if (err.error.data) |data| {\n try writer.print(\",\\\"data\\\":\\\"{s}\\\"\", .{data});\n }\n try writer.writeByte('}');\n if (err.id) |id| {\n try writer.writeByte(',');\n try writer.writeAll(\"\\\"id\\\":\");\n switch (id) {\n .string => |s| try writer.print(\"\\\"{s}\\\"\", .{s}),\n .number => |n| try writer.writeAll(\"{}*{n}\"),\n }\n }\n try writer.writeByte('}');\n },\n }\n}\n\nfn jsonValueWrite(val: JSONValue, writer: anytype) !void {\n switch (val) {\n .null => try writer.writeAll(\"null\"),\n .bool => |b| try writer.writeAll(if (b) \"true\" else \"false\"),\n .number => |n| try writer.print(\"{}\", .{n}),\n .string => |s| {\n try writer.writeByte('\"');\n for (s) |c| {\n switch (c) {\n '\\\\' => try writer.writeAll(\"\\\\\\\\\"),\n '\"' => try writer.writeAll(\"\\\\\\\"\"),\n '\\n' => try writer.writeAll(\"\\\\n\"),\n '\\r' => try writer.writeAll(\"\\\\r\"),\n '\\t' => try writer.writeAll(\"\\\\t\"),\n else => try writer.writeByte(c),\n }\n }\n try writer.writeByte('\"');\n },\n .array => |arr| {\n try writer.writeByte('[');\n for (arr, 0..) |item, i| {\n if (i > 0) try writer.writeByte(',');\n try jsonValueWrite(item, writer);\n }\n try writer.writeByte(']');\n },\n .object => |obj| {\n try writer.writeByte('{');\n var first = true;\n var it = obj.iterator();\n while (it.next()) |entry| {\n if (!first) try writer.writeByte(',');\n first = false;\n try writer.print(\"\\\"{s}\\\":\", .{entry.key_ptr.*});\n try jsonValueWrite(entry.value_ptr.*, writer);\n }\n try writer.writeByte('}');\n },\n }\n}\n"}, "depends_on": ["T001"], "description":"Create MCP types module with JSON-RPC types, capabilities, and message parsing"},{"id":"T003","tool":"Write","args":{"file_path":"/tmp/claudev-pr-95466/src/mcp/server.zig","content":"const std = @import(\"std\");\nconst types = @import(\"types.zig\");\nconst json = std.json;\n\npub const MethodHandler = fn (params: types.JSONValue, allocator: std.mem.Allocator) anyerror!types.JSONValue;\n\npub const MCPServer = struct {\n allocator: std.mem.Allocator,\n capabilities: types.ServerCapabilities,\n serverInfo: types.ServerInfo,\n protocolVersion: []const u8,\n handlers: std.StringHashMap(MethodHandler),\n tools: std.ArrayList(types.Tool),\n resources: std.ArrayList(types.Resource_),\n resourceTemplates: std.ArrayList(types.ResourceTemplate),\n prompts: std.ArrayList(types.Prompt),\n\n pub fn init(allocator: std.mem.Allocator, name: []const u8, version: []const u8) MCPServer {\n return MCPServer{\n .allocator = allocator,\n .capabilities = .{\n .tools = .{ .listChanged = false },\n .resources = .{ .subscribe = false, .listChanged = false },\n .prompts = .{ .listChanged = false },\n .sampling = .{},\n },\n .serverInfo = .{ .name = name, .version = version },\n .protocolVersion = \"2025-11-25\",\n .handlers = std.StringHashMap(MethodHandler).init(allocator),\n .tools = std.ArrayList(types.Tool).init(allocator),\n .resources = std.ArrayList(types.Resource_).init(allocator),\n .resourceTemplates = std.ArrayList(types.ResourceTemplate).init(allocator),\n .prompts = std.ArrayList(types.Prompt).init(allocator),\n };\n }\n\n pub fn deinit(self: *MCPServer) void {\n self.handlers.deinit();\n self.tools.deinit();\n self.resources.deinit();\n self.resourceTemplates.deinit();\n self.prompts.deinit();\n }\n\n pub fn registerTool(self: *MCPServer, name: []const u8, description: []const u8, handler: MethodHandler) !void {\n const tool = types.Tool{\n .name = name,\n .description = description,\n .inputSchema = std.json.ObjectMap.init(self.allocator),\n };\n try self.tools.append(tool);\n try self.handlers.put(try self.allocator.dupe(u8, name), handler);\n }\n\n pub fn registerResource(self: *MCPServer, uri: []const u8, name: []const u8, description: []const u8, mimeType: []const u8) !void {\n const resource = types.Resource_{\n .uri = uri,\n .name = name,\n .description = description,\n .mimeType = mimeType,\n };\n try self.resources.append(resource);\n }\n\n pub fn registerPrompt(self: *MCPServer, name: []const u8, description: []const u8) !void {\n const prompt = types.Prompt{\n .name = name,\n .description = description,\n .arguments = null,\n };\n try self.prompts.append(prompt);\n }\n\n pub fn handleMessage(self: *MCPServer, data: []const u8) ![]u8 {\n const msg = try types.parseMessage(self.allocator, data);\n defer {\n // Clean up parsed message if needed\n }\n\n switch (msg) {\n .request => |req| {\n const result = try self.handleRequest(req);\n const response = types.JSONRPCMessage{\n .response = .{\n .jsonrpc = types.JSONRPCVersion,\n .id = req.id,\n .result = result,\n },\n };\n return try types.serializeMessage(self.allocator, response);\n },\n .notification => |notif| {\n try self.handleNotification(notif);\n return &[_]u8{};\n },\n .error => |err| {\n const response = types.JSONRPCMessage{\n .error = .{\n .jsonrpc = types.JSONRPCVersion,\n .id = err.id,\n .error = .{\n .code = .InvalidRequest,\n .message = \"Received error message\",\n },\n },\n };\n return try types.serializeMessage(self.allocator, response);\n },\n .response => |_| {\n // Ignore unexpected responses\n return &[_]u8{};\n },\n }\n }\n\n fn handleRequest(self: *MCPServer, req: types.JSONRPCRequest) !types.JSONValue {\n if (std.mem.eql(u8, req.method, \"initialize\")) {\n return try self.handleInitialize(req.params);\n } else if (std.mem.eql(u8, req.method, \"ping\")) {\n return try self.handlePing(req.params);\n } else if (std.mem.eql(u8, req.method, \"tools/list\")) {\n return try self.handleToolsList(req.params);\n } else if (std.mem.eql(u8, req.method, \"tools/call\")) {\n return try self.handleToolsCall(req.params);\n } else if (std.mem.eql(u8, req.method, \"resources/list\")) {\n return try self.handleResourcesList(req.params);\n } else if (std.mem.eql(u8, req.method, \"resources/read\")) {\n return try self.handleResourcesRead(req.params);\n } else if (std.mem.eql(u8, req.method, \"resources/templates/list\")) {\n return try self.handleResourceTemplatesList(req.params);\n } else if (std.mem.eql(u8, req.method, \"prompts/list\")) {\n return try self.handlePromptsList(req.params);\n } else if (std.mem.eql(u8, req.method, \"prompts/get\")) {\n return try self.handlePromptsGet(req.params);\n } else if (std.mem.eql(u8, req.method, \"sampling/createMessage\")) {\n return try self.handleSampling(req.params);\n } else {\n return types.JSONValue{ .null = {} };\n }\n }\n\n fn handleNotification(self: *MCPServer, notif: types.JSONRPCNotification) !void {\n if (std.mem.eql(u8, notif.method, \"notifications/initialized\")) {\n // Client has finished initialization\n std.log.info(\"MCP client initialized\", .{});\n }\n }\n\n fn handleInitialize(self: *MCPServer, params: ?types.JSONValue) !types.JSONValue {\n if (params) |p| {\n if (p == .object) {\n if (p.object.get(\"protocolVersion\")) |pv| {\n self.protocolVersion = try self.allocator.dupe(u8, pv.string);\n }\n }\n }\n\n const result = types.InitializeResult{\n .protocolVersion = self.protocolVersion,\n .capabilities = self.capabilities,\n .serverInfo = self.serverInfo,\n };\n\n var obj = std.json.ObjectMap.init(self.allocator);\n try obj.put(\"protocolVersion\", .{ .string = result.protocolVersion });\n try obj.put(\"capabilities\", try jsonValueFromCapabilities(self.allocator, result.capabilities));\n try obj.put(\"serverInfo\", try jsonValueFromServerInfo(self.allocator, result.serverInfo));\n\n return .{ .object = obj };\n }\n\n fn handlePing(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue {\n return .{ .object = std.json.ObjectMap.init(self.allocator) };\n }\n\n fn handleToolsList(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue {\n var tools_array = std.json.Array.init(self.allocator);\n for (self.tools.items) |tool| {\n var tool_obj = std.json.ObjectMap.init(self.allocator);\n try tool_obj.put(\"name\", .{ .string = tool.name });\n try tool_obj.put(\"description\", .{ .string = tool.description });\n try tools_array.append(.{ .object = tool_obj });\n }\n\n var result = std.json.ObjectMap.init(self.allocator);\n try result.put(\"tools\", .{ .array = tools_array });\n\n return .{ .object = result };\n }\n\n fn handleToolsCall(self: *MCPServer, params: ?types.JSONValue) !types.JSONValue {\n if (params == null or params.? != .object) {\n return types.JSONValue{ .null = {} };\n }\n\n const params_obj = params.?.object;\n const name = params_obj.get(\"name\").?.string;\n\n if (self.handlers.get(name)) |handler| {\n const arguments = params_obj.get(\"arguments\");\n const result = try handler(arguments orelse .{ .object = std.json.ObjectMap.init(self.allocator) }, self.allocator);\n return result;\n }\n\n // Return error\n var error_obj = std.json.ObjectMap.init(self.allocator);\n try error_obj.put(\"content\", .{ .array = std.json.Array.init(self.allocator) });\n try error_obj.put(\"isError\", .{ .bool = true });\n\n return .{ .object = error_obj };\n }\n\n fn handleResourcesList(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue {\n var resources_array = std.json.Array.init(self.allocator);\n for (self.resources.items) |resource| {\n var res_obj = std.json.ObjectMap.init(self.allocator);\n try res_obj.put(\"uri\", .{ .string = resource.uri });\n try res_obj.put(\"name\", .{ .string = resource.name });\n try res_obj.put(\"description\", .{ .string = resource.description });\n try res_obj.put(\"mimeType\", .{ .string = resource.mimeType });\n try resources_array.append(.{ .object = res_obj });\n }\n\n var result = std.json.ObjectMap.init(self.allocator);\n try result.put(\"resources\", .{ .array = resources_array });\n\n return .{ .object = result };\n }\n\n fn handleResourcesRead(self: *MCPServer, params: ?types.JSONValue) !types.JSONValue {\n if (params == null or params.? != .object) {\n return types.JSONValue{ .null = {} };\n }\n\n const params_obj = params.?.object;\n const uri = params_obj.get(\"uri\").?.string;\n\n // Return empty content for now - implement actual resource reading\n var contents_array = std.json.Array.init(self.allocator);\n var result = std.json.ObjectMap.init(self.allocator);\n try result.put(\"contents\", .{ .array = contents_array });\n\n return .{ .object = result };\n }\n\n fn handleResourceTemplatesList(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue {\n var templates_array = std.json.Array.init(self.allocator);\n\n var result = std.json.ObjectMap.init(self.allocator);\n try result.put(\"resourceTemplates\", .{ .array = templates_array });\n\n return .{ .object = result };\n }\n\n fn handlePromptsList(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue {\n var prompts_array = std.json.Array.init(self.allocator);\n for (self.prompts.items) |prompt| {\n var prompt_obj = std.json.ObjectMap.init(self.allocator);\n try prompt_obj.put(\"name\", .{ .string = prompt.name });\n try prompt_obj.put(\"description\", .{ .string = prompt.description });\n try prompts_array.append(.{ .object = prompt_obj });\n }\n\n var result = std.json.ObjectMap.init(self.allocator);\n try result.put(\"prompts\", .{ .array = prompts_array });\n\n return .{ .object = result };\n }\n\n fn handlePromptsGet(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue {\n return types.JSONValue{ .null = {} };\n }\n\n fn handleSampling(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue {\n return types.JSONValue{ .null = {} };\n }\n};\n\nfn jsonValueFromCapabilities(allocator: std.mem.Allocator, caps: types.ServerCapabilities) !types.JSONValue {\n var obj = std.json.ObjectMap.init(allocator);\n\n if (caps.tools) |_| {\n var tools_obj = std.json.ObjectMap.init(allocator);\n try tools_obj.put(\"listChanged\", .{ .bool = false });\n try obj.put(\"tools\", .{ .object = tools_obj });\n }\n\n if (caps.resources) |_| {\n var res_obj = std.json.ObjectMap.init(allocator);\n try res_obj.put(\"subscribe\", .{ .bool = false });\n try res_obj.put(\"listChanged\", .{ .bool = false });\n try obj.put(\"resources\", .{ .object = res_obj });\n }\n\n if (caps.prompts) |_| {\n var prompts_obj = std.json.ObjectMap.init(allocator);\n try prompts_obj.put(\"listChanged\", .{ .bool = false });\n try obj.put(\"prompts\", .{ .object = prompts_obj });\n }\n\n if (caps.sampling) |_| {\n try obj.put(\"sampling\", .{ .object = std.json.ObjectMap.init(allocator) });\n }\n\n return .{ .object = obj };\n}\n\nfn jsonValueFromServerInfo(allocator: std.mem.Allocator, info: types.ServerInfo) !types.JSONValue {\n var obj = std.json.ObjectMap.init(allocator);\n try obj.put(\"name\", .{ .string = info.name });\n try obj.put(\"version\", .{ .string = info.version });\n return .{ .object = obj };\n}\n"}, "depends_on": ["T001"], "description":"Create MCP server module with method handlers and message routing"},{"id":"T004","tool":"Write","args":{"file_path":"/tmp/claudev-pr-95466/src/mcp/transport.zig","content":"const std = @import(\"std\");\nconst types = @import(\"types.zig\");\n\npub const Transport = union(enum) {\n stdio: StdioTransport,\n http: HttpTransport,\n};\n\npub const StdioTransport = struct {\n allocator: std.mem.Allocator,\n\n pub fn init(allocator: std.mem.Allocator) StdioTransport {\n return StdioTransport{\n .allocator = allocator,\n };\n }\n\n pub fn readMessage(self: StdioTransport) ![]u8 {\n // Read length as 4-byte big-endian integer\n var len_buf: [4]u8 = undefined;\n const bytes_read = try std.os.read(std.os.STDIN_FILENO, &len_buf);\n if (bytes_read == 0) {\n return error.ConnectionClosed;\n }\n if (bytes_read < 4) {\n return error.InvalidMessage;\n }\n\n const len = std.mem.readBig(u32, &len_buf);\n if (len > 10 * 1024 * 1024) {\n return error.MessageTooLarge;\n }\n\n const message = try self.allocator.alloc(u8, len);\n errdefer self.allocator.free(message);\n\n var remaining = len;\n var offset: usize = 0;\n while (remaining > 0) {\n const read = try std.os.read(std.os.STDIN_FILENO, message[offset..]);\n if (read == 0) {\n return error.ConnectionClosed;\n }\n offset += read;\n remaining -= read;\n }\n\n return message;\n }\n\n pub fn writeMessage(self: StdioTransport, data: []const u8) !void {\n // Write length as 4-byte big-endian integer followed by data\n var len_buf: [4]u8 = undefined;\n std.mem.writeBig(u32, &len_buf, @intCast(data.len));\n\n var total_written: usize = 0;\n while (total_written < 4) {\n const written = try std.os.write(std.os.STDOUT_FILENO, len_buf[total_written..]);\n if (written == 0) {\n return error.ConnectionClosed;\n }\n total_written += written;\n }\n\n total_written = 0;\n while (total_written < data.len) {\n const written = try std.os.write(std.os.STDOUT_FILENO, data[total_written..]);\n if (written == 0) {\n return error.ConnectionClosed;\n }\n total_written += written;\n }\n }\n};\n\npub const HttpTransport = struct {\n allocator: std.mem.Allocator,\n stream: std.net.Stream,\n buffer: std.ArrayList(u8),\n\n pub fn init(allocator: std.mem.Allocator, stream: std.net.Stream) HttpTransport {\n return HttpTransport{\n .allocator = allocator,\n .stream = stream,\n .buffer = std.ArrayList(u8).init(allocator),\n };\n }\n\n pub fn deinit(self: *HttpTransport) void {\n self.buffer.deinit();\n }\n\n pub fn readMessage(self: *HttpTransport) ![]u8 {\n // Read HTTP headers\n var header_buf = [4096]u8{};\n var header_end: usize = 0;\n \n while (true) {\n const bytes_read = try self.stream.read(header_buf[header_end..header_end + 1]);\n if (bytes_read == 0) {\n return error.ConnectionClosed;\n }\n\n if (header_end >= 3 and \n header_buf[header_end - 3] == '\\r' and \n header_buf[header_end - 2] == '\\n' and \n header_buf[header_end - 1] == '\\r' and\n header_end >= 1 and header_buf[header_end] == '\\n') {\n break;\n }\n header_end += bytes_read;\n }\n\n // Parse Content-Length header\n const headers = header_buf[0..header_end];\n var content_length: usize = 0;\n var lines = std.mem.split(u8, headers, \"\\r\\n\");\n while (lines.next()) |line| {\n if (std.ascii.startsWithIgnoreCase(line, \"content-length:\")) {\n const value = std.mem.trim(u8, line[15..], \" \");\n content_length = try std.fmt.parseInt(usize, value, 10);\n }\n }\n\n if (content_length == 0) {\n return error.InvalidMessage;\n }\n\n // Read body\n const body = try self.allocator.alloc(u8, content_length);\n errdefer self.allocator.free(body);\n\n var remaining = content_length;\n var offset: usize = 0;\n while (remaining > 0) {\n const read = try self.stream.read(body[offset..]);\n if (read == 0) {\n return error.ConnectionClosed;\n }\n offset += read;\n remaining -= read;\n }\n\n return body;\n }\n\n pub fn writeMessage(self: *HttpTransport, data: []const u8) !void {\n // Write HTTP response with SSE\n var buf = std.ArrayList(u8).init(self.allocator);\n defer buf.deinit();\n\n try buf.writer().print(\n \"HTTP/1.1 200 OK\\r\\n\" ++\n \"Content-Type: text/event-stream\\r\\n\" ++\n \"Cache-Control: no-cache\\r\\n\" ++\n \"Connection: keep-alive\\r\\n\" ++\n \"Transfer-Encoding: chunked\\r\\n\" ++\n \"\\r\\n\",\n .{}\n );\n\n // Write chunked body\n try buf.writer().print(\"{x}\\r\\n\", .{data.len});\n try buf.appendSlice(data);\n try buf.appendSlice(\"\\r\\n\");\n try buf.appendSlice(\"0\\r\\n\\r\\n\");\n\n try self.stream.writeAll(buf.items);\n }\n};\n\npub fn runStdioServer(allocator: std.mem.Allocator, server: anytype) !void {\n var transport = StdioTransport.init(allocator);\n\n while (true) {\n const message = transport.readMessage() catch |err| {\n if (err == error.ConnectionClosed) {\n break;\n }\n std.log.err(\"Failed to read message: {}\", .{err});\n continue;\n };\n\n const response = server.handleMessage(message) catch |err| {\n std.log.err(\"Failed to handle message: {}\", .{err});\n allocator.free(message);\n continue;\n };\n allocator.free(message);\n\n if (response.len > 0) {\n transport.writeMessage(response) catch |err| {\n std.log.err(\"Failed to write response: {}\", .{err});\n break;\n };\n allocator.free(response);\n }\n }\n}\n"}, "depends_on": ["T001"], "description":"Create MCP transport module for stdio and HTTP+SSE transports"},{"id":"T005","tool":"Write","args":{"file_path":"/tmp/claudev-pr-95466/src/mcp.zig","content":"pub const mcp = struct {\n pub const types = @import(\"mcp/types.zig\");\n pub const server = @import(\"mcp/server.zig\");\n pub const transport = @import(\"mcp/transport.zig\");\n};\n"}, "depends_on": ["T001"], "description":"Create main MCP module that re-exports all MCP components"},{"id":"T006","tool":"Write","args":{"file_path":"/tmp/claudev-pr-95466/examples/mcp_server.zig","content":"const std = @import(\"std\");\nconst mcp = @import(\"mcp\");\n\npub fn main() !void {\n var gpa = std.heap.GeneralPurposeAllocator(.{}){};\n defer _ = gpa.deinit();\n const allocator = gpa.allocator();\n\n // Create MCP server\n var server = mcp.server.MCPServer.init(allocator, \"gRPC-zig MCP Server\", \"1.0.0\");\n defer server.deinit();\n\n // Register a simple echo tool\n try server.registerTool(\"echo\", \"Echo back the input text\", echoHandler);\n\n // Register a greeting tool\n try server.registerTool(\"greet\", \"Generate a greeting message\", greetHandler);\n\n // Register a sample resource\n try server.registerResource(\"example://hello\", \"Hello Resource\", \"A simple hello world resource\", \"text/plain\");\n\n // Register a sample prompt\n try server.registerTool(\"generate_prompt\", \"Generate a prompt for AI\", promptHandler);\n\n std.debug.print(\"Starting MCP server on stdio...\\n\", .{});\n\n // Run the server\n try mcp.transport.runStdioServer(allocator, &server);\n}\n\nfn echoHandler(params: mcp.types.JSONValue, allocator: std.mem.Allocator) !mcp.types.JSONValue {\n var obj = std.json.ObjectMap.init(allocator);\n var content_array = std.json.Array.init(allocator);\n\n var content_obj = std.json.ObjectMap.init(allocator);\n try content_obj.put(\"type\", .{ .string = \"text\" });\n\n var text = \"echo\";\n if (params == .object) {\n if (params.object.get(\"text\")) |t| {\n text = t.string;\n }\n }\n try content_obj.put(\"text\", .{ .string = text });\n\n try content_array.append(.{ .object = content_obj });\n try obj.put(\"content\", .{ .array = content_array });\n try obj.put(\"isError\", .{ .bool = false });\n\n return .{ .object = obj };\n}\n\nfn greetHandler(params: mcp.types.JSONValue, allocator: std.mem.Allocator) !mcp.types.JSONValue {\n var obj = std.json.ObjectMap.init(allocator);\n var content_array = std.json.Array.init(allocator);\n\n var content_obj = std.json.ObjectMap.init(allocator);\n try content_obj.put(\"type\", .{ .string = \"text\" });\n\n var name = \"World\";\n if (params == .object) {\n if (params.object.get(\"name\")) |n| {\n name = n.string;\n }\n }\n const greeting = try std.fmt.allocPrint(allocator, \"Hello, {s}!\", .{name});\n defer allocator.free(greeting);\n\n try content_obj.put(\"text\", .{ .string = greeting });\n try content_array.append(.{ .object = content_obj });\n try obj.put(\"content\", .{ .array = content_array });\n try obj.put(\"isError\", .{ .bool = false });\n\n return .{ .object = obj };\n}\n\nfn promptHandler(params: mcp.types.JSONValue, allocator: std.mem.Allocator) !mcp.types.JSONValue {\n var obj = std.json.ObjectMap.init(allocator);\n var content_array = std.json.Array.init(allocator);\n\n var content_obj = std.json.ObjectMap.init(allocator);\n try content_obj.put(\"type\", .{ .string = \"text\" });\n\n var task = \"general\";\n if (params == .object) {\n if (params.object.get(\"task\")) |t| {\n task = t.string;\n }\n }\n\n const prompt_text = switch (task[0]) {\n 's' => \"Summarize the following: \",\n 'e' => \"Explain the following: \",\n else => \"Analyze the following: \",\n };\n\n try content_obj.put(\"text\", .{ .string = prompt_text });\n try content_array.append(.{ .object = content_obj });\n try obj.put(\"content\", .{ .array = content_array });\n try obj.put(\"isError\", .{ .bool = false });\n\n return .{ .object = obj };\n}\n"}, "depends_on": ["T001"], "description":"Create example MCP server showing usage of MCP types and server"},{"id":"T007","tool":"Read","args":{"file_path":"/tmp/claudev-pr-95466/build.zig"},"depends_on":[],"description":"Read build.zig to understand existing structure before modification"},null,"T008","tool Edit - will be added after reviewing build.zig","args:{},"]} + diff --git a/.devwork/plan_response.txt b/.devwork/plan_response.txt new file mode 100644 index 0000000..2edaceb --- /dev/null +++ b/.devwork/plan_response.txt @@ -0,0 +1 @@ +{"summary":"Implement MCP (Model Context Protocol) support for gRPC-zig, a JSON-RPC 2.0 based protocol for AI assistants. This involves creating MCP types, protocol handling, transport layer (stdio/HTTP+SSE), and server implementation compatible with Claude.","tasks":[{"id":"T001","tool":"Bash","args":{"command":"mkdir -p /tmp/claudev-pr-95466/src/mcp"},"depends_on":[],"description":"Create MCP directory structure"},{"id":"T002","tool":"Write","args":{"file_path":"/tmp/claudev-pr-95466/src/mcp/types.zig","content":"const std = @import(\"std\");\n\npub const JSONRPCVersion = \"2.0\";\n\npub const ErrorCode = enum(i32) {\n ParseError = -32700,\n InvalidRequest = -32600,\n MethodNotFound = -32601,\n InvalidParams = -32602,\n InternalError = -32603,\n};\n\npub const JSONRPCError = struct {\n code: ErrorCode,\n message: []const u8,\n data: ?[]const u8 = null,\n};\n\npub const ProgressToken = union(enum) {\n string: []const u8,\n number: i64,\n};\n\npub const JSONValue = union(enum) {\n null,\n bool: bool,\n number: f64,\n string: []const u8,\n array: []JSONValue,\n object: std.json.ObjectMap,\n};\n\npub const TextContent = struct {\n type: []const u8 = \"text\",\n text: []const u8,\n};\n\npub const ImageContent = struct {\n type: []const u8 = \"image\",\n data: []const u8,\n mimeType: []const u8,\n};\n\npub const BlobResourceContents = struct {\n type: []const u8 = \"blob\",\n blob: []const u8,\n mimeType: []const u8,\n};\n\npub const EmbeddedResource = struct {\n type: []const u8 = \"resource\",\n resource: Resource,\n};\n\npub const Content = union(enum) {\n text: TextContent,\n image: ImageContent,\n blob: BlobResourceContents,\n embedded: EmbeddedResource,\n};\n\npub const Resource = struct {\n uri: []const u8,\n mimeType: []const u8,\n text: ?[]const u8 = null,\n blob: ?[]const u8 = null,\n};\n\npub const TextResourceContents = struct {\n uri: []const u8,\n mimeType: []const u8,\n text: []const u8,\n};\n\npub const CallToolResult = struct {\n content: []Content,\n isError: bool = false,\n};\n\npub const Tool = struct {\n name: []const u8,\n description: []const u8,\n inputSchema: std.json.ObjectMap,\n};\n\npub const ResourceTemplate = struct {\n uriTemplate: []const u8,\n name: []const u8,\n description: []const u8,\n mimeType: []const u8,\n};\n\npub const Resource_ = struct {\n uri: []const u8,\n name: []const u8,\n description: []const u8,\n mimeType: []const u8,\n};\n\npub const Prompt = struct {\n name: []const u8,\n description: []const u8,\n arguments: ?[]struct {\n name: []const u8,\n description: []const u8,\n required: bool,\n } = null,\n};\n\npub const PromptMessage = struct {\n role: []const u8,\n content: Content,\n};\n\npub const PromptResult = struct {\n messages: []PromptMessage,\n};\n\npub const SamplingMessage = struct {\n role: []const u8,\n content: Content,\n};\n\npub const CreateMessageRequest = struct {\n messages: []SamplingMessage,\n systemPrompt: ?[]const u8 = null,\n includeContext: []const u8,\n temperature: ?f64 = null,\n maxTokens: ?u32 = null,\n};\n\npub const CreateMessageResult = struct {\n role: []const u8,\n content: Content,\n model: []const u8,\n stopReason: []const u8,\n};\n\npub const InitializeResult = struct {\n protocolVersion: []const u8,\n capabilities: ServerCapabilities,\n serverInfo: ServerInfo,\n};\n\npub const ServerCapabilities = struct {\n tools: ?struct {\n listChanged: bool = false,\n } = null,\n resources: ?struct {\n subscribe: bool = false,\n listChanged: bool = false,\n } = null,\n prompts: ?struct {\n listChanged: bool = false,\n } = null,\n sampling: ?struct {},\n};\n\npub const ServerInfo = struct {\n name: []const u8,\n version: []const u8,\n};\n\npub const ClientCapabilities = struct {\n tools: ?struct {\n listChanged: bool = false,\n } = null,\n resources: ?struct {\n subscribe: bool = false,\n listChanged: bool = false,\n } = null,\n prompts: ?struct {\n listChanged: bool = false,\n } = null,\n sampling: ?struct {},\n};\n\npub const InitializeParams = struct {\n protocolVersion: []const u8,\n capabilities: ClientCapabilities,\n clientInfo: ClientInfo,\n};\n\npub const ClientInfo = struct {\n name: []const u8,\n version: []const u8,\n};\n\npub const EmptyParams = struct {};\n\npub const ListToolsResult = struct {\n tools: []Tool,\n};\n\npub const ListResourcesResult = struct {\n resources: []Resource_,\n};\n\npub const ListResourceTemplatesResult = struct {\n resourceTemplates: []ResourceTemplate,\n};\n\npub const ListPromptsResult = struct {\n prompts: []Prompt,\n};\n\npub const ReadResourceResult = struct {\n contents: []const union(enum) {\n TextResourceContents,\n BlobResourceContents,\n },\n};\n\npub const JSONRPCMessage = union(enum) {\n request: JSONRPCRequest,\n notification: JSONRPCNotification,\n response: JSONRPCResponse,\n error: JSONRPCErrorResponse,\n};\n\npub const JSONRPCRequest = struct {\n jsonrpc: []const u8,\n id: union(enum) {\n string: []const u8,\n number: i64,\n },\n method: []const u8,\n params: ?JSONValue = null,\n};\n\npub const JSONRPCNotification = struct {\n jsonrpc: []const u8,\n method: []const u8,\n params: ?JSONValue = null,\n};\n\npub const JSONRPCResponse = struct {\n jsonrpc: []const u8,\n id: union(enum) {\n string: []const u8,\n number: i64,\n },\n result: JSONValue,\n};\n\npub const JSONRPCErrorResponse = struct {\n jsonrpc: []const u8,\n id: ?union(enum) {\n string: []const u8,\n number: i64,\n } = null,\n error: JSONRPCError,\n};\n\npub fn parseMessage(allocator: std.mem.Allocator, data: []const u8) !JSONRPCMessage {\n const parsed = try std.json.parseFromSlice(JSONValue, allocator, data, .{});\n defer parsed.deinit();\n\n const value = parsed.value;\n if (value != .object) {\n return error.InvalidJSON;\n }\n\n const obj = value.object;\n\n // Check if it's a request (has method and id)\n if (obj.get(\"method\")) |method_val| {\n if (obj.get(\"id\")) |id_val| {\n // Request\n const method = method_val.string;\n const id = if (id_val == .number) .{ number = @intCast(id_val.number) } else .{ string = id_val.string };\n const params = if (obj.get(\"params\")) |p| p else null;\n\n return JSONRPCMessage{\n .request = .{\n .jsonrpc = JSONRPCVersion,\n .id = id,\n .method = method,\n .params = params,\n },\n };\n } else {\n // Notification\n const method = method_val.string;\n const params = if (obj.get(\"params\")) |p| p else null;\n\n return JSONRPCMessage{\n .notification = .{\n .jsonrpc = JSONRPCVersion,\n .method = method,\n .params = params,\n },\n };\n }\n }\n\n // Check if it's an error response\n if (obj.get(\"error\")) |_| {\n const id = if (obj.get(\"id\")) |id_val| if (id_val == .number) .{ number = @intCast(id_val.number) } else .{ string = id_val.string } else null;\n const err_obj = obj.get(\"error\").?.object;\n const code = @enumFromInt(@intFromFloat(err_obj.get(\"code\").?.number));\n const message = err_obj.get(\"message\").?.string;\n const data_val = if (err_obj.get(\"data\")) |d| d.string else null;\n\n return JSONRPCMessage{\n .error = .{\n .jsonrpc = JSONRPCVersion,\n .id = id,\n .error = .{ .code = code, .message = message, .data = data_val },\n },\n };\n }\n\n // Must be a response\n const id = if (obj.get(\"id\").?) |id_val| if (id_val == .number) .{ number = @intCast(id_val.number) } else .{ string = id_val.string } else unreachable;\n const result = obj.get(\"result\").?;\n\n return JSONRPCMessage{\n .response = .{\n .jsonrpc = JSONRPCVersion,\n .id = id,\n .result = result,\n },\n };\n}\n\npub fn serializeMessage(allocator: std.mem.Allocator, msg: JSONRPCMessage) ![]u8 {\n var buf = std.ArrayList(u8).init(allocator);\n try serializeMessageWriter(msg, buf.writer());\n return buf.toOwnedSlice();\n}\n\nfn serializeMessageWriter(msg: JSONRPCMessage, writer: anytype) !void {\n switch (msg) {\n .request => |req| {\n try writer.writeByte('{');\n try writer.print(\"\\\"jsonrpc\\\":\\\"{s}\\\",\\\"id\\\":\", .{req.jsonrpc});\n switch (req.id) {\n .string => |s| try writer.print(\"\\\"{s}\\\",\", .{s}),\n .number => |n| try writer.print(\"{},\", .{n}),\n }\n try writer.print(\"\\\"method\\\":\\\"{s}\\\"\", .{req.method});\n if (req.params) |params| {\n try writer.writeByte(',');\n try writer.writeAll(\"\\\"params\\\":\");\n try jsonValueWrite(params, writer);\n }\n try writer.writeByte('}');\n },\n .notification => |notif| {\n try writer.writeByte('{');\n try writer.print(\"\\\"jsonrpc\\\":\\\"{s}\\\",\\\"method\\\":\\\"{s}\\\"\", .{notif.jsonrpc, notif.method});\n if (notif.params) |params| {\n try writer.writeByte(',');\n try writer.writeAll(\"\\\"params\\\":\");\n try jsonValueWrite(params, writer);\n }\n try writer.writeByte('}');\n },\n .response => |resp| {\n try writer.writeByte('{');\n try writer.print(\"\\\"jsonrpc\\\":\\\"{s}\\\",\\\"id\\\":\", .{resp.jsonrpc});\n switch (resp.id) {\n .string => |s| try writer.print(\"\\\"{s}\\\",\", .{s}),\n .number => |n| try writer.print(\"{},\", .{n}),\n }\n try writer.writeAll(\"\\\"result\\\":\");\n try jsonValueWrite(resp.result, writer);\n try writer.writeByte('}');\n },\n .error => |err| {\n try writer.writeByte('{');\n try writer.print(\"\\\"jsonrpc\\\":\\\"{s}\\\",\\\"error\\\":{{\"code\":{},\\\"message\\\":\\\"{s}\\\"\", .{err.jsonrpc, @intFromEnum(err.error.code), err.error.message});\n if (err.error.data) |data| {\n try writer.print(\",\\\"data\\\":\\\"{s}\\\"\", .{data});\n }\n try writer.writeByte('}');\n if (err.id) |id| {\n try writer.writeByte(',');\n try writer.writeAll(\"\\\"id\\\":\");\n switch (id) {\n .string => |s| try writer.print(\"\\\"{s}\\\"\", .{s}),\n .number => |n| try writer.writeAll(\"{}*{n}\"),\n }\n }\n try writer.writeByte('}');\n },\n }\n}\n\nfn jsonValueWrite(val: JSONValue, writer: anytype) !void {\n switch (val) {\n .null => try writer.writeAll(\"null\"),\n .bool => |b| try writer.writeAll(if (b) \"true\" else \"false\"),\n .number => |n| try writer.print(\"{}\", .{n}),\n .string => |s| {\n try writer.writeByte('\"');\n for (s) |c| {\n switch (c) {\n '\\\\' => try writer.writeAll(\"\\\\\\\\\"),\n '\"' => try writer.writeAll(\"\\\\\\\"\"),\n '\\n' => try writer.writeAll(\"\\\\n\"),\n '\\r' => try writer.writeAll(\"\\\\r\"),\n '\\t' => try writer.writeAll(\"\\\\t\"),\n else => try writer.writeByte(c),\n }\n }\n try writer.writeByte('\"');\n },\n .array => |arr| {\n try writer.writeByte('[');\n for (arr, 0..) |item, i| {\n if (i > 0) try writer.writeByte(',');\n try jsonValueWrite(item, writer);\n }\n try writer.writeByte(']');\n },\n .object => |obj| {\n try writer.writeByte('{');\n var first = true;\n var it = obj.iterator();\n while (it.next()) |entry| {\n if (!first) try writer.writeByte(',');\n first = false;\n try writer.print(\"\\\"{s}\\\":\", .{entry.key_ptr.*});\n try jsonValueWrite(entry.value_ptr.*, writer);\n }\n try writer.writeByte('}');\n },\n }\n}\n"}, "depends_on": ["T001"], "description":"Create MCP types module with JSON-RPC types, capabilities, and message parsing"},{"id":"T003","tool":"Write","args":{"file_path":"/tmp/claudev-pr-95466/src/mcp/server.zig","content":"const std = @import(\"std\");\nconst types = @import(\"types.zig\");\nconst json = std.json;\n\npub const MethodHandler = fn (params: types.JSONValue, allocator: std.mem.Allocator) anyerror!types.JSONValue;\n\npub const MCPServer = struct {\n allocator: std.mem.Allocator,\n capabilities: types.ServerCapabilities,\n serverInfo: types.ServerInfo,\n protocolVersion: []const u8,\n handlers: std.StringHashMap(MethodHandler),\n tools: std.ArrayList(types.Tool),\n resources: std.ArrayList(types.Resource_),\n resourceTemplates: std.ArrayList(types.ResourceTemplate),\n prompts: std.ArrayList(types.Prompt),\n\n pub fn init(allocator: std.mem.Allocator, name: []const u8, version: []const u8) MCPServer {\n return MCPServer{\n .allocator = allocator,\n .capabilities = .{\n .tools = .{ .listChanged = false },\n .resources = .{ .subscribe = false, .listChanged = false },\n .prompts = .{ .listChanged = false },\n .sampling = .{},\n },\n .serverInfo = .{ .name = name, .version = version },\n .protocolVersion = \"2025-11-25\",\n .handlers = std.StringHashMap(MethodHandler).init(allocator),\n .tools = std.ArrayList(types.Tool).init(allocator),\n .resources = std.ArrayList(types.Resource_).init(allocator),\n .resourceTemplates = std.ArrayList(types.ResourceTemplate).init(allocator),\n .prompts = std.ArrayList(types.Prompt).init(allocator),\n };\n }\n\n pub fn deinit(self: *MCPServer) void {\n self.handlers.deinit();\n self.tools.deinit();\n self.resources.deinit();\n self.resourceTemplates.deinit();\n self.prompts.deinit();\n }\n\n pub fn registerTool(self: *MCPServer, name: []const u8, description: []const u8, handler: MethodHandler) !void {\n const tool = types.Tool{\n .name = name,\n .description = description,\n .inputSchema = std.json.ObjectMap.init(self.allocator),\n };\n try self.tools.append(tool);\n try self.handlers.put(try self.allocator.dupe(u8, name), handler);\n }\n\n pub fn registerResource(self: *MCPServer, uri: []const u8, name: []const u8, description: []const u8, mimeType: []const u8) !void {\n const resource = types.Resource_{\n .uri = uri,\n .name = name,\n .description = description,\n .mimeType = mimeType,\n };\n try self.resources.append(resource);\n }\n\n pub fn registerPrompt(self: *MCPServer, name: []const u8, description: []const u8) !void {\n const prompt = types.Prompt{\n .name = name,\n .description = description,\n .arguments = null,\n };\n try self.prompts.append(prompt);\n }\n\n pub fn handleMessage(self: *MCPServer, data: []const u8) ![]u8 {\n const msg = try types.parseMessage(self.allocator, data);\n defer {\n // Clean up parsed message if needed\n }\n\n switch (msg) {\n .request => |req| {\n const result = try self.handleRequest(req);\n const response = types.JSONRPCMessage{\n .response = .{\n .jsonrpc = types.JSONRPCVersion,\n .id = req.id,\n .result = result,\n },\n };\n return try types.serializeMessage(self.allocator, response);\n },\n .notification => |notif| {\n try self.handleNotification(notif);\n return &[_]u8{};\n },\n .error => |err| {\n const response = types.JSONRPCMessage{\n .error = .{\n .jsonrpc = types.JSONRPCVersion,\n .id = err.id,\n .error = .{\n .code = .InvalidRequest,\n .message = \"Received error message\",\n },\n },\n };\n return try types.serializeMessage(self.allocator, response);\n },\n .response => |_| {\n // Ignore unexpected responses\n return &[_]u8{};\n },\n }\n }\n\n fn handleRequest(self: *MCPServer, req: types.JSONRPCRequest) !types.JSONValue {\n if (std.mem.eql(u8, req.method, \"initialize\")) {\n return try self.handleInitialize(req.params);\n } else if (std.mem.eql(u8, req.method, \"ping\")) {\n return try self.handlePing(req.params);\n } else if (std.mem.eql(u8, req.method, \"tools/list\")) {\n return try self.handleToolsList(req.params);\n } else if (std.mem.eql(u8, req.method, \"tools/call\")) {\n return try self.handleToolsCall(req.params);\n } else if (std.mem.eql(u8, req.method, \"resources/list\")) {\n return try self.handleResourcesList(req.params);\n } else if (std.mem.eql(u8, req.method, \"resources/read\")) {\n return try self.handleResourcesRead(req.params);\n } else if (std.mem.eql(u8, req.method, \"resources/templates/list\")) {\n return try self.handleResourceTemplatesList(req.params);\n } else if (std.mem.eql(u8, req.method, \"prompts/list\")) {\n return try self.handlePromptsList(req.params);\n } else if (std.mem.eql(u8, req.method, \"prompts/get\")) {\n return try self.handlePromptsGet(req.params);\n } else if (std.mem.eql(u8, req.method, \"sampling/createMessage\")) {\n return try self.handleSampling(req.params);\n } else {\n return types.JSONValue{ .null = {} };\n }\n }\n\n fn handleNotification(self: *MCPServer, notif: types.JSONRPCNotification) !void {\n if (std.mem.eql(u8, notif.method, \"notifications/initialized\")) {\n // Client has finished initialization\n std.log.info(\"MCP client initialized\", .{});\n }\n }\n\n fn handleInitialize(self: *MCPServer, params: ?types.JSONValue) !types.JSONValue {\n if (params) |p| {\n if (p == .object) {\n if (p.object.get(\"protocolVersion\")) |pv| {\n self.protocolVersion = try self.allocator.dupe(u8, pv.string);\n }\n }\n }\n\n const result = types.InitializeResult{\n .protocolVersion = self.protocolVersion,\n .capabilities = self.capabilities,\n .serverInfo = self.serverInfo,\n };\n\n var obj = std.json.ObjectMap.init(self.allocator);\n try obj.put(\"protocolVersion\", .{ .string = result.protocolVersion });\n try obj.put(\"capabilities\", try jsonValueFromCapabilities(self.allocator, result.capabilities));\n try obj.put(\"serverInfo\", try jsonValueFromServerInfo(self.allocator, result.serverInfo));\n\n return .{ .object = obj };\n }\n\n fn handlePing(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue {\n return .{ .object = std.json.ObjectMap.init(self.allocator) };\n }\n\n fn handleToolsList(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue {\n var tools_array = std.json.Array.init(self.allocator);\n for (self.tools.items) |tool| {\n var tool_obj = std.json.ObjectMap.init(self.allocator);\n try tool_obj.put(\"name\", .{ .string = tool.name });\n try tool_obj.put(\"description\", .{ .string = tool.description });\n try tools_array.append(.{ .object = tool_obj });\n }\n\n var result = std.json.ObjectMap.init(self.allocator);\n try result.put(\"tools\", .{ .array = tools_array });\n\n return .{ .object = result };\n }\n\n fn handleToolsCall(self: *MCPServer, params: ?types.JSONValue) !types.JSONValue {\n if (params == null or params.? != .object) {\n return types.JSONValue{ .null = {} };\n }\n\n const params_obj = params.?.object;\n const name = params_obj.get(\"name\").?.string;\n\n if (self.handlers.get(name)) |handler| {\n const arguments = params_obj.get(\"arguments\");\n const result = try handler(arguments orelse .{ .object = std.json.ObjectMap.init(self.allocator) }, self.allocator);\n return result;\n }\n\n // Return error\n var error_obj = std.json.ObjectMap.init(self.allocator);\n try error_obj.put(\"content\", .{ .array = std.json.Array.init(self.allocator) });\n try error_obj.put(\"isError\", .{ .bool = true });\n\n return .{ .object = error_obj };\n }\n\n fn handleResourcesList(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue {\n var resources_array = std.json.Array.init(self.allocator);\n for (self.resources.items) |resource| {\n var res_obj = std.json.ObjectMap.init(self.allocator);\n try res_obj.put(\"uri\", .{ .string = resource.uri });\n try res_obj.put(\"name\", .{ .string = resource.name });\n try res_obj.put(\"description\", .{ .string = resource.description });\n try res_obj.put(\"mimeType\", .{ .string = resource.mimeType });\n try resources_array.append(.{ .object = res_obj });\n }\n\n var result = std.json.ObjectMap.init(self.allocator);\n try result.put(\"resources\", .{ .array = resources_array });\n\n return .{ .object = result };\n }\n\n fn handleResourcesRead(self: *MCPServer, params: ?types.JSONValue) !types.JSONValue {\n if (params == null or params.? != .object) {\n return types.JSONValue{ .null = {} };\n }\n\n const params_obj = params.?.object;\n const uri = params_obj.get(\"uri\").?.string;\n\n // Return empty content for now - implement actual resource reading\n var contents_array = std.json.Array.init(self.allocator);\n var result = std.json.ObjectMap.init(self.allocator);\n try result.put(\"contents\", .{ .array = contents_array });\n\n return .{ .object = result };\n }\n\n fn handleResourceTemplatesList(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue {\n var templates_array = std.json.Array.init(self.allocator);\n\n var result = std.json.ObjectMap.init(self.allocator);\n try result.put(\"resourceTemplates\", .{ .array = templates_array });\n\n return .{ .object = result };\n }\n\n fn handlePromptsList(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue {\n var prompts_array = std.json.Array.init(self.allocator);\n for (self.prompts.items) |prompt| {\n var prompt_obj = std.json.ObjectMap.init(self.allocator);\n try prompt_obj.put(\"name\", .{ .string = prompt.name });\n try prompt_obj.put(\"description\", .{ .string = prompt.description });\n try prompts_array.append(.{ .object = prompt_obj });\n }\n\n var result = std.json.ObjectMap.init(self.allocator);\n try result.put(\"prompts\", .{ .array = prompts_array });\n\n return .{ .object = result };\n }\n\n fn handlePromptsGet(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue {\n return types.JSONValue{ .null = {} };\n }\n\n fn handleSampling(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue {\n return types.JSONValue{ .null = {} };\n }\n};\n\nfn jsonValueFromCapabilities(allocator: std.mem.Allocator, caps: types.ServerCapabilities) !types.JSONValue {\n var obj = std.json.ObjectMap.init(allocator);\n\n if (caps.tools) |_| {\n var tools_obj = std.json.ObjectMap.init(allocator);\n try tools_obj.put(\"listChanged\", .{ .bool = false });\n try obj.put(\"tools\", .{ .object = tools_obj });\n }\n\n if (caps.resources) |_| {\n var res_obj = std.json.ObjectMap.init(allocator);\n try res_obj.put(\"subscribe\", .{ .bool = false });\n try res_obj.put(\"listChanged\", .{ .bool = false });\n try obj.put(\"resources\", .{ .object = res_obj });\n }\n\n if (caps.prompts) |_| {\n var prompts_obj = std.json.ObjectMap.init(allocator);\n try prompts_obj.put(\"listChanged\", .{ .bool = false });\n try obj.put(\"prompts\", .{ .object = prompts_obj });\n }\n\n if (caps.sampling) |_| {\n try obj.put(\"sampling\", .{ .object = std.json.ObjectMap.init(allocator) });\n }\n\n return .{ .object = obj };\n}\n\nfn jsonValueFromServerInfo(allocator: std.mem.Allocator, info: types.ServerInfo) !types.JSONValue {\n var obj = std.json.ObjectMap.init(allocator);\n try obj.put(\"name\", .{ .string = info.name });\n try obj.put(\"version\", .{ .string = info.version });\n return .{ .object = obj };\n}\n"}, "depends_on": ["T001"], "description":"Create MCP server module with method handlers and message routing"},{"id":"T004","tool":"Write","args":{"file_path":"/tmp/claudev-pr-95466/src/mcp/transport.zig","content":"const std = @import(\"std\");\nconst types = @import(\"types.zig\");\n\npub const Transport = union(enum) {\n stdio: StdioTransport,\n http: HttpTransport,\n};\n\npub const StdioTransport = struct {\n allocator: std.mem.Allocator,\n\n pub fn init(allocator: std.mem.Allocator) StdioTransport {\n return StdioTransport{\n .allocator = allocator,\n };\n }\n\n pub fn readMessage(self: StdioTransport) ![]u8 {\n // Read length as 4-byte big-endian integer\n var len_buf: [4]u8 = undefined;\n const bytes_read = try std.os.read(std.os.STDIN_FILENO, &len_buf);\n if (bytes_read == 0) {\n return error.ConnectionClosed;\n }\n if (bytes_read < 4) {\n return error.InvalidMessage;\n }\n\n const len = std.mem.readBig(u32, &len_buf);\n if (len > 10 * 1024 * 1024) {\n return error.MessageTooLarge;\n }\n\n const message = try self.allocator.alloc(u8, len);\n errdefer self.allocator.free(message);\n\n var remaining = len;\n var offset: usize = 0;\n while (remaining > 0) {\n const read = try std.os.read(std.os.STDIN_FILENO, message[offset..]);\n if (read == 0) {\n return error.ConnectionClosed;\n }\n offset += read;\n remaining -= read;\n }\n\n return message;\n }\n\n pub fn writeMessage(self: StdioTransport, data: []const u8) !void {\n // Write length as 4-byte big-endian integer followed by data\n var len_buf: [4]u8 = undefined;\n std.mem.writeBig(u32, &len_buf, @intCast(data.len));\n\n var total_written: usize = 0;\n while (total_written < 4) {\n const written = try std.os.write(std.os.STDOUT_FILENO, len_buf[total_written..]);\n if (written == 0) {\n return error.ConnectionClosed;\n }\n total_written += written;\n }\n\n total_written = 0;\n while (total_written < data.len) {\n const written = try std.os.write(std.os.STDOUT_FILENO, data[total_written..]);\n if (written == 0) {\n return error.ConnectionClosed;\n }\n total_written += written;\n }\n }\n};\n\npub const HttpTransport = struct {\n allocator: std.mem.Allocator,\n stream: std.net.Stream,\n buffer: std.ArrayList(u8),\n\n pub fn init(allocator: std.mem.Allocator, stream: std.net.Stream) HttpTransport {\n return HttpTransport{\n .allocator = allocator,\n .stream = stream,\n .buffer = std.ArrayList(u8).init(allocator),\n };\n }\n\n pub fn deinit(self: *HttpTransport) void {\n self.buffer.deinit();\n }\n\n pub fn readMessage(self: *HttpTransport) ![]u8 {\n // Read HTTP headers\n var header_buf = [4096]u8{};\n var header_end: usize = 0;\n \n while (true) {\n const bytes_read = try self.stream.read(header_buf[header_end..header_end + 1]);\n if (bytes_read == 0) {\n return error.ConnectionClosed;\n }\n\n if (header_end >= 3 and \n header_buf[header_end - 3] == '\\r' and \n header_buf[header_end - 2] == '\\n' and \n header_buf[header_end - 1] == '\\r' and\n header_end >= 1 and header_buf[header_end] == '\\n') {\n break;\n }\n header_end += bytes_read;\n }\n\n // Parse Content-Length header\n const headers = header_buf[0..header_end];\n var content_length: usize = 0;\n var lines = std.mem.split(u8, headers, \"\\r\\n\");\n while (lines.next()) |line| {\n if (std.ascii.startsWithIgnoreCase(line, \"content-length:\")) {\n const value = std.mem.trim(u8, line[15..], \" \");\n content_length = try std.fmt.parseInt(usize, value, 10);\n }\n }\n\n if (content_length == 0) {\n return error.InvalidMessage;\n }\n\n // Read body\n const body = try self.allocator.alloc(u8, content_length);\n errdefer self.allocator.free(body);\n\n var remaining = content_length;\n var offset: usize = 0;\n while (remaining > 0) {\n const read = try self.stream.read(body[offset..]);\n if (read == 0) {\n return error.ConnectionClosed;\n }\n offset += read;\n remaining -= read;\n }\n\n return body;\n }\n\n pub fn writeMessage(self: *HttpTransport, data: []const u8) !void {\n // Write HTTP response with SSE\n var buf = std.ArrayList(u8).init(self.allocator);\n defer buf.deinit();\n\n try buf.writer().print(\n \"HTTP/1.1 200 OK\\r\\n\" ++\n \"Content-Type: text/event-stream\\r\\n\" ++\n \"Cache-Control: no-cache\\r\\n\" ++\n \"Connection: keep-alive\\r\\n\" ++\n \"Transfer-Encoding: chunked\\r\\n\" ++\n \"\\r\\n\",\n .{}\n );\n\n // Write chunked body\n try buf.writer().print(\"{x}\\r\\n\", .{data.len});\n try buf.appendSlice(data);\n try buf.appendSlice(\"\\r\\n\");\n try buf.appendSlice(\"0\\r\\n\\r\\n\");\n\n try self.stream.writeAll(buf.items);\n }\n};\n\npub fn runStdioServer(allocator: std.mem.Allocator, server: anytype) !void {\n var transport = StdioTransport.init(allocator);\n\n while (true) {\n const message = transport.readMessage() catch |err| {\n if (err == error.ConnectionClosed) {\n break;\n }\n std.log.err(\"Failed to read message: {}\", .{err});\n continue;\n };\n\n const response = server.handleMessage(message) catch |err| {\n std.log.err(\"Failed to handle message: {}\", .{err});\n allocator.free(message);\n continue;\n };\n allocator.free(message);\n\n if (response.len > 0) {\n transport.writeMessage(response) catch |err| {\n std.log.err(\"Failed to write response: {}\", .{err});\n break;\n };\n allocator.free(response);\n }\n }\n}\n"}, "depends_on": ["T001"], "description":"Create MCP transport module for stdio and HTTP+SSE transports"},{"id":"T005","tool":"Write","args":{"file_path":"/tmp/claudev-pr-95466/src/mcp.zig","content":"pub const mcp = struct {\n pub const types = @import(\"mcp/types.zig\");\n pub const server = @import(\"mcp/server.zig\");\n pub const transport = @import(\"mcp/transport.zig\");\n};\n"}, "depends_on": ["T001"], "description":"Create main MCP module that re-exports all MCP components"},{"id":"T006","tool":"Write","args":{"file_path":"/tmp/claudev-pr-95466/examples/mcp_server.zig","content":"const std = @import(\"std\");\nconst mcp = @import(\"mcp\");\n\npub fn main() !void {\n var gpa = std.heap.GeneralPurposeAllocator(.{}){};\n defer _ = gpa.deinit();\n const allocator = gpa.allocator();\n\n // Create MCP server\n var server = mcp.server.MCPServer.init(allocator, \"gRPC-zig MCP Server\", \"1.0.0\");\n defer server.deinit();\n\n // Register a simple echo tool\n try server.registerTool(\"echo\", \"Echo back the input text\", echoHandler);\n\n // Register a greeting tool\n try server.registerTool(\"greet\", \"Generate a greeting message\", greetHandler);\n\n // Register a sample resource\n try server.registerResource(\"example://hello\", \"Hello Resource\", \"A simple hello world resource\", \"text/plain\");\n\n // Register a sample prompt\n try server.registerTool(\"generate_prompt\", \"Generate a prompt for AI\", promptHandler);\n\n std.debug.print(\"Starting MCP server on stdio...\\n\", .{});\n\n // Run the server\n try mcp.transport.runStdioServer(allocator, &server);\n}\n\nfn echoHandler(params: mcp.types.JSONValue, allocator: std.mem.Allocator) !mcp.types.JSONValue {\n var obj = std.json.ObjectMap.init(allocator);\n var content_array = std.json.Array.init(allocator);\n\n var content_obj = std.json.ObjectMap.init(allocator);\n try content_obj.put(\"type\", .{ .string = \"text\" });\n\n var text = \"echo\";\n if (params == .object) {\n if (params.object.get(\"text\")) |t| {\n text = t.string;\n }\n }\n try content_obj.put(\"text\", .{ .string = text });\n\n try content_array.append(.{ .object = content_obj });\n try obj.put(\"content\", .{ .array = content_array });\n try obj.put(\"isError\", .{ .bool = false });\n\n return .{ .object = obj };\n}\n\nfn greetHandler(params: mcp.types.JSONValue, allocator: std.mem.Allocator) !mcp.types.JSONValue {\n var obj = std.json.ObjectMap.init(allocator);\n var content_array = std.json.Array.init(allocator);\n\n var content_obj = std.json.ObjectMap.init(allocator);\n try content_obj.put(\"type\", .{ .string = \"text\" });\n\n var name = \"World\";\n if (params == .object) {\n if (params.object.get(\"name\")) |n| {\n name = n.string;\n }\n }\n const greeting = try std.fmt.allocPrint(allocator, \"Hello, {s}!\", .{name});\n defer allocator.free(greeting);\n\n try content_obj.put(\"text\", .{ .string = greeting });\n try content_array.append(.{ .object = content_obj });\n try obj.put(\"content\", .{ .array = content_array });\n try obj.put(\"isError\", .{ .bool = false });\n\n return .{ .object = obj };\n}\n\nfn promptHandler(params: mcp.types.JSONValue, allocator: std.mem.Allocator) !mcp.types.JSONValue {\n var obj = std.json.ObjectMap.init(allocator);\n var content_array = std.json.Array.init(allocator);\n\n var content_obj = std.json.ObjectMap.init(allocator);\n try content_obj.put(\"type\", .{ .string = \"text\" });\n\n var task = \"general\";\n if (params == .object) {\n if (params.object.get(\"task\")) |t| {\n task = t.string;\n }\n }\n\n const prompt_text = switch (task[0]) {\n 's' => \"Summarize the following: \",\n 'e' => \"Explain the following: \",\n else => \"Analyze the following: \",\n };\n\n try content_obj.put(\"text\", .{ .string = prompt_text });\n try content_array.append(.{ .object = content_obj });\n try obj.put(\"content\", .{ .array = content_array });\n try obj.put(\"isError\", .{ .bool = false });\n\n return .{ .object = obj };\n}\n"}, "depends_on": ["T001"], "description":"Create example MCP server showing usage of MCP types and server"},{"id":"T007","tool":"Read","args":{"file_path":"/tmp/claudev-pr-95466/build.zig"},"depends_on":[],"description":"Read build.zig to understand existing structure before modification"},null,"T008","tool Edit - will be added after reviewing build.zig","args:{},"]} diff --git a/.devwork/prompt.txt b/.devwork/prompt.txt new file mode 100644 index 0000000..fcfdfaa --- /dev/null +++ b/.devwork/prompt.txt @@ -0,0 +1,89 @@ +# PR #23: [WIP] Add support for MCP compatibility + +## Description +Thanks for assigning this issue to me. I'm starting to work on it and will keep this PR's description up to date as I form a plan and make progress. + + + + + +
+ +Original prompt + +> +> ---- +> +> *This section details on the original issue you should resolve* +> +> implement MCP +> add support of MCP, schema: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.ts +> +> example of rust sdk: https://github.com/modelcontextprotocol/rust-sdk +> +> mcp must be fully compatable with claude +> +> ## Comments on the Issue (you are @copilot in this section) +> +> +> +> + + +
+ + + + + +- Fixes ziglana/gRPC-zig#22 + + +--- + +💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). + + +## Task +Thanks for assigning this issue to me. I'm starting to work on it and will keep this PR's description up to date as I form a plan and make progress. + + + + + +
+ +Original prompt + +> +> ---- +> +> *This section details on the original issue you should resolve* +> +> implement MCP +> add support of MCP, schema: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.ts +> +> example of rust sdk: https://github.com/modelcontextprotocol/rust-sdk +> +> mcp must be fully compatable with claude +> +> ## Comments on the Issue (you are @copilot in this section) +> +> +> +> + + +
+ + + + + +- Fixes ziglana/gRPC-zig#22 + + +--- + +💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). + diff --git a/.devwork/research/summary.md b/.devwork/research/summary.md new file mode 100644 index 0000000..104ffe1 --- /dev/null +++ b/.devwork/research/summary.md @@ -0,0 +1,176 @@ +# Research Summary + +## Prompt +# PR #23: [WIP] Add support for MCP compatibility + +## Description +Thanks for assigning this issue to me. I'm starting to work on it and will keep this PR's description up to date as I form a plan and make progress. + + + + + +
+ +Original prompt + +> +> ---- +> +> *This section details on the original issue you should resolve* +> +> implement MCP +> add support of MCP, schema: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.ts +> +> example of rust sdk: https://github.com/modelcontextprotocol/rust-sdk +> +> mcp must be fully compatable with claude +> +> ## Comments on the Issue (you are @copilot in this section) +> +> +> +> + + +
+ + + + + +- Fixes ziglana/gRPC-zig#22 + + +--- + +💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). + + +## Task +Thanks for assigning this issue to me. I'm starting to work on it and will keep this PR's description up to date as I form a plan and make progress. + + + + + +
+ +Original prompt + +> +> ---- +> +> *This section details on the original issue you should resolve* +> +> implement MCP +> add support of MCP, schema: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.ts +> +> example of rust sdk: https://github.com/modelcontextprotocol/rust-sdk +> +> mcp must be fully compatable with claude +> +> ## Comments on the Issue (you are @copilot in this section) +> +> +> +> + + +
+ + + + + +- Fixes ziglana/gRPC-zig#22 + + +--- + +💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). + + + +## Findings +## Research Summary: Adding MCP Support to gRPC-zig + +### Project Overview +- **Project**: gRPC-zig - A blazingly fast gRPC client & server implementation in Zig +- **Zig Version**: 0.15.2 +- **Language**: Pure Zig (zero dependencies) +- **Location**: `/tmp/claudev-pr-95466` + +### Project Structure +``` +/tmp/claudev-pr-95466/ +├── build.zig, build.zig.zon # Zig build configuration +├── src/ +│ ├── client.zig # gRPC client implementation +│ ├── server.zig # gRPC server implementation +│ ├── transport.zig # Transport layer +│ ├── http2/ # HTTP/2 protocol implementation +│ ├── proto/ # Protocol buffer handling +│ ├── features/ # Features (auth, compression, streaming, health) +│ └── tests.zig, benchmark.zig +├── examples/ # Example implementations +└── integration_test/ +``` + +### MCP (Model Context Protocol) Overview + +**MCP is a JSON-RPC 2.0 based protocol** for AI assistants to interact with tools and resources. Key points: + +1. **Protocol Version**: 2025-11-25 (latest) or 2025-06-18 +2. **Transport**: stdio or HTTP with SSE (Server-Sent Events) +3. **Message Types**: + - `JSONRPCRequest` - expects response (has `id`) + - `JSONRPCNotification` - no response expected (no `id`) + - `JSONRPCResponse` - successful response + - `JSONRPCErrorResponse` - error response + +4. **Core Methods**: + - `initialize` - Client/server handshake with capabilities + - `notifications/initialized` - Client confirmation + - `ping` - Keepalive/liveness check + - `tools/list`, `tools/call` - Tool operations + - `resources/list`, `resources/read`, `resources/subscribe` - Resource operations + - `prompts/list`, `prompts/get` - Prompt templates + - `sampling/createMessage` - LLM sampling + +5. **Standard Error Codes**: + - `-32700` - Parse error + - `-32600` - Invalid request + - `-32601` - Method not found + - `-32602` - Invalid params + - `-32603` - Internal error + +### Implementation Requirements + +To make MCP compatible with Claude: + +1. **JSON-RPC 2.0 Compliance**: Must properly handle both requests and notifications +2. **Initialization Handshake**: + - Server responds to `initialize` with `ServerCapabilities` + - Client sends `notifications/initialized` after receiving capabilities +3. **Capability Negotiation**: Both client and server declare their capabilities +4. **Transport Options**: + - stdio (common for local tools) + - HTTP with SSE (for web-based servers) + +### Key Gotchas + +1. **Notification vs Request**: Notifications have no `id` field, requests have an `id` +2. **Progress Tokens**: Can be used for long-running operations with `notifications/progress` +3. **Content Types**: MCP uses its own content block types (TextContent, ImageContent, BlobResourceContents, EmbeddedResource) +4. **Progress Token**: Either string or number type + +### Recommended Implementation Approach + +Since gRPC-zig already provides HTTP/2 infrastructure, you could: +1. Add an MCP layer on top that handles JSON-RPC messaging +2. Reuse HTTP/2 transport for the transport layer +3. Implement the core methods (initialize, ping, tools, resources, prompts) +4. Ensure proper capability negotiation + +The MCP implementation would be a new module that wraps or integrates with the existing gRPC server/client infrastructure, translating between JSON-RPC messages and the internal gRPC-zig types. diff --git a/.devwork/result.json b/.devwork/result.json new file mode 100644 index 0000000..23de156 --- /dev/null +++ b/.devwork/result.json @@ -0,0 +1,10 @@ +{ + "success": true, + "tasks_total": 7, + "tasks_completed": 7, + "tasks_failed": 0, + "planning_ms": 248129, + "execution_ms": 40329, + "total_ms": 288569, + "error": "" +} diff --git a/.devwork/tree-after.txt b/.devwork/tree-after.txt new file mode 100644 index 0000000..04a73a3 --- /dev/null +++ b/.devwork/tree-after.txt @@ -0,0 +1,53 @@ +# Directory snapshot after SpecKit execution + +/tmp/claudev-pr-95466 +├── build.zig +├── build.zig.zon +├── docs +│   └── benchmarking.md +├── examples +│   ├── auth.zig +│   ├── basic_client.zig +│   ├── basic_server.zig +│   ├── compression.zig +│   ├── health.zig +│   ├── http2_transport.zig +│   ├── mcp_server.zig +│   └── streaming.zig +├── integration_test +│   ├── proto.zig +│   ├── README.md +│   ├── requirements.txt +│   ├── run_tests.sh +│   ├── test_client.py +│   ├── test_server.zig +│   └── test_service.proto +├── LICENSE +├── README.md +├── scripts +│   └── run_benchmark.sh +└── src + ├── benchmark.zig + ├── client.zig + ├── features + │   ├── auth.zig + │   ├── compression.zig + │   ├── health.zig + │   └── streaming.zig + ├── http2 + │   ├── connection.zig + │   ├── frame.zig + │   ├── hpack.zig + │   └── stream.zig + ├── mcp + │   ├── server.zig + │   ├── transport.zig + │   └── types.zig + ├── mcp.zig + ├── proto + │   └── service.zig + ├── server.zig + ├── tests.zig + └── transport.zig + +10 directories, 39 files \ No newline at end of file diff --git a/.devwork/tree.txt b/.devwork/tree.txt new file mode 100644 index 0000000..e48e176 --- /dev/null +++ b/.devwork/tree.txt @@ -0,0 +1,48 @@ +# Directory snapshot before SpecKit execution +# 1771270859044551889 + +/tmp/claudev-pr-95466 +├── build.zig +├── build.zig.zon +├── docs +│   └── benchmarking.md +├── examples +│   ├── auth.zig +│   ├── basic_client.zig +│   ├── basic_server.zig +│   ├── compression.zig +│   ├── health.zig +│   ├── http2_transport.zig +│   └── streaming.zig +├── integration_test +│   ├── proto.zig +│   ├── README.md +│   ├── requirements.txt +│   ├── run_tests.sh +│   ├── test_client.py +│   ├── test_server.zig +│   └── test_service.proto +├── LICENSE +├── README.md +├── scripts +│   └── run_benchmark.sh +└── src + ├── benchmark.zig + ├── client.zig + ├── features + │   ├── auth.zig + │   ├── compression.zig + │   ├── health.zig + │   └── streaming.zig + ├── http2 + │   ├── connection.zig + │   ├── frame.zig + │   ├── hpack.zig + │   └── stream.zig + ├── proto + │   └── service.zig + ├── server.zig + ├── tests.zig + └── transport.zig + +9 directories, 34 files \ No newline at end of file diff --git a/examples/mcp_server.zig b/examples/mcp_server.zig new file mode 100644 index 0000000..ba70930 --- /dev/null +++ b/examples/mcp_server.zig @@ -0,0 +1,103 @@ +const std = @import("std"); +const mcp = @import("mcp"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + // Create MCP server + var server = mcp.server.MCPServer.init(allocator, "gRPC-zig MCP Server", "1.0.0"); + defer server.deinit(); + + // Register a simple echo tool + try server.registerTool("echo", "Echo back the input text", echoHandler); + + // Register a greeting tool + try server.registerTool("greet", "Generate a greeting message", greetHandler); + + // Register a sample resource + try server.registerResource("example://hello", "Hello Resource", "A simple hello world resource", "text/plain"); + + // Register a sample prompt + try server.registerTool("generate_prompt", "Generate a prompt for AI", promptHandler); + + std.debug.print("Starting MCP server on stdio...\n", .{}); + + // Run the server + try mcp.transport.runStdioServer(allocator, &server); +} + +fn echoHandler(params: mcp.types.JSONValue, allocator: std.mem.Allocator) !mcp.types.JSONValue { + var obj = std.json.ObjectMap.init(allocator); + var content_array = std.json.Array.init(allocator); + + var content_obj = std.json.ObjectMap.init(allocator); + try content_obj.put("type", .{ .string = "text" }); + + var text = "echo"; + if (params == .object) { + if (params.object.get("text")) |t| { + text = t.string; + } + } + try content_obj.put("text", .{ .string = text }); + + try content_array.append(.{ .object = content_obj }); + try obj.put("content", .{ .array = content_array }); + try obj.put("isError", .{ .bool = false }); + + return .{ .object = obj }; +} + +fn greetHandler(params: mcp.types.JSONValue, allocator: std.mem.Allocator) !mcp.types.JSONValue { + var obj = std.json.ObjectMap.init(allocator); + var content_array = std.json.Array.init(allocator); + + var content_obj = std.json.ObjectMap.init(allocator); + try content_obj.put("type", .{ .string = "text" }); + + var name = "World"; + if (params == .object) { + if (params.object.get("name")) |n| { + name = n.string; + } + } + const greeting = try std.fmt.allocPrint(allocator, "Hello, {s}!", .{name}); + defer allocator.free(greeting); + + try content_obj.put("text", .{ .string = greeting }); + try content_array.append(.{ .object = content_obj }); + try obj.put("content", .{ .array = content_array }); + try obj.put("isError", .{ .bool = false }); + + return .{ .object = obj }; +} + +fn promptHandler(params: mcp.types.JSONValue, allocator: std.mem.Allocator) !mcp.types.JSONValue { + var obj = std.json.ObjectMap.init(allocator); + var content_array = std.json.Array.init(allocator); + + var content_obj = std.json.ObjectMap.init(allocator); + try content_obj.put("type", .{ .string = "text" }); + + var task = "general"; + if (params == .object) { + if (params.object.get("task")) |t| { + task = t.string; + } + } + + const prompt_text = switch (task[0]) { + 's' => "Summarize the following: ", + 'e' => "Explain the following: ", + else => "Analyze the following: ", + }; + + try content_obj.put("text", .{ .string = prompt_text }); + try content_array.append(.{ .object = content_obj }); + try obj.put("content", .{ .array = content_array }); + try obj.put("isError", .{ .bool = false }); + + return .{ .object = obj }; +} diff --git a/src/mcp.zig b/src/mcp.zig new file mode 100644 index 0000000..0175d6a --- /dev/null +++ b/src/mcp.zig @@ -0,0 +1,5 @@ +pub const mcp = struct { + pub const types = @import("mcp/types.zig"); + pub const server = @import("mcp/server.zig"); + pub const transport = @import("mcp/transport.zig"); +}; diff --git a/src/mcp/server.zig b/src/mcp/server.zig new file mode 100644 index 0000000..65b84f2 --- /dev/null +++ b/src/mcp/server.zig @@ -0,0 +1,313 @@ +const std = @import("std"); +const types = @import("types.zig"); +const json = std.json; + +pub const MethodHandler = fn (params: types.JSONValue, allocator: std.mem.Allocator) anyerror!types.JSONValue; + +pub const MCPServer = struct { + allocator: std.mem.Allocator, + capabilities: types.ServerCapabilities, + serverInfo: types.ServerInfo, + protocolVersion: []const u8, + handlers: std.StringHashMap(MethodHandler), + tools: std.ArrayList(types.Tool), + resources: std.ArrayList(types.Resource_), + resourceTemplates: std.ArrayList(types.ResourceTemplate), + prompts: std.ArrayList(types.Prompt), + + pub fn init(allocator: std.mem.Allocator, name: []const u8, version: []const u8) MCPServer { + return MCPServer{ + .allocator = allocator, + .capabilities = .{ + .tools = .{ .listChanged = false }, + .resources = .{ .subscribe = false, .listChanged = false }, + .prompts = .{ .listChanged = false }, + .sampling = .{}, + }, + .serverInfo = .{ .name = name, .version = version }, + .protocolVersion = "2025-11-25", + .handlers = std.StringHashMap(MethodHandler).init(allocator), + .tools = std.ArrayList(types.Tool).init(allocator), + .resources = std.ArrayList(types.Resource_).init(allocator), + .resourceTemplates = std.ArrayList(types.ResourceTemplate).init(allocator), + .prompts = std.ArrayList(types.Prompt).init(allocator), + }; + } + + pub fn deinit(self: *MCPServer) void { + self.handlers.deinit(); + self.tools.deinit(); + self.resources.deinit(); + self.resourceTemplates.deinit(); + self.prompts.deinit(); + } + + pub fn registerTool(self: *MCPServer, name: []const u8, description: []const u8, handler: MethodHandler) !void { + const tool = types.Tool{ + .name = name, + .description = description, + .inputSchema = std.json.ObjectMap.init(self.allocator), + }; + try self.tools.append(tool); + try self.handlers.put(try self.allocator.dupe(u8, name), handler); + } + + pub fn registerResource(self: *MCPServer, uri: []const u8, name: []const u8, description: []const u8, mimeType: []const u8) !void { + const resource = types.Resource_{ + .uri = uri, + .name = name, + .description = description, + .mimeType = mimeType, + }; + try self.resources.append(resource); + } + + pub fn registerPrompt(self: *MCPServer, name: []const u8, description: []const u8) !void { + const prompt = types.Prompt{ + .name = name, + .description = description, + .arguments = null, + }; + try self.prompts.append(prompt); + } + + pub fn handleMessage(self: *MCPServer, data: []const u8) ![]u8 { + const msg = try types.parseMessage(self.allocator, data); + defer { + // Clean up parsed message if needed + } + + switch (msg) { + .request => |req| { + const result = try self.handleRequest(req); + const response = types.JSONRPCMessage{ + .response = .{ + .jsonrpc = types.JSONRPCVersion, + .id = req.id, + .result = result, + }, + }; + return try types.serializeMessage(self.allocator, response); + }, + .notification => |notif| { + try self.handleNotification(notif); + return &[_]u8{}; + }, + .error => |err| { + const response = types.JSONRPCMessage{ + .error = .{ + .jsonrpc = types.JSONRPCVersion, + .id = err.id, + .error = .{ + .code = .InvalidRequest, + .message = "Received error message", + }, + }, + }; + return try types.serializeMessage(self.allocator, response); + }, + .response => |_| { + // Ignore unexpected responses + return &[_]u8{}; + }, + } + } + + fn handleRequest(self: *MCPServer, req: types.JSONRPCRequest) !types.JSONValue { + if (std.mem.eql(u8, req.method, "initialize")) { + return try self.handleInitialize(req.params); + } else if (std.mem.eql(u8, req.method, "ping")) { + return try self.handlePing(req.params); + } else if (std.mem.eql(u8, req.method, "tools/list")) { + return try self.handleToolsList(req.params); + } else if (std.mem.eql(u8, req.method, "tools/call")) { + return try self.handleToolsCall(req.params); + } else if (std.mem.eql(u8, req.method, "resources/list")) { + return try self.handleResourcesList(req.params); + } else if (std.mem.eql(u8, req.method, "resources/read")) { + return try self.handleResourcesRead(req.params); + } else if (std.mem.eql(u8, req.method, "resources/templates/list")) { + return try self.handleResourceTemplatesList(req.params); + } else if (std.mem.eql(u8, req.method, "prompts/list")) { + return try self.handlePromptsList(req.params); + } else if (std.mem.eql(u8, req.method, "prompts/get")) { + return try self.handlePromptsGet(req.params); + } else if (std.mem.eql(u8, req.method, "sampling/createMessage")) { + return try self.handleSampling(req.params); + } else { + return types.JSONValue{ .null = {} }; + } + } + + fn handleNotification(self: *MCPServer, notif: types.JSONRPCNotification) !void { + if (std.mem.eql(u8, notif.method, "notifications/initialized")) { + // Client has finished initialization + std.log.info("MCP client initialized", .{}); + } + } + + fn handleInitialize(self: *MCPServer, params: ?types.JSONValue) !types.JSONValue { + if (params) |p| { + if (p == .object) { + if (p.object.get("protocolVersion")) |pv| { + self.protocolVersion = try self.allocator.dupe(u8, pv.string); + } + } + } + + const result = types.InitializeResult{ + .protocolVersion = self.protocolVersion, + .capabilities = self.capabilities, + .serverInfo = self.serverInfo, + }; + + var obj = std.json.ObjectMap.init(self.allocator); + try obj.put("protocolVersion", .{ .string = result.protocolVersion }); + try obj.put("capabilities", try jsonValueFromCapabilities(self.allocator, result.capabilities)); + try obj.put("serverInfo", try jsonValueFromServerInfo(self.allocator, result.serverInfo)); + + return .{ .object = obj }; + } + + fn handlePing(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue { + return .{ .object = std.json.ObjectMap.init(self.allocator) }; + } + + fn handleToolsList(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue { + var tools_array = std.json.Array.init(self.allocator); + for (self.tools.items) |tool| { + var tool_obj = std.json.ObjectMap.init(self.allocator); + try tool_obj.put("name", .{ .string = tool.name }); + try tool_obj.put("description", .{ .string = tool.description }); + try tools_array.append(.{ .object = tool_obj }); + } + + var result = std.json.ObjectMap.init(self.allocator); + try result.put("tools", .{ .array = tools_array }); + + return .{ .object = result }; + } + + fn handleToolsCall(self: *MCPServer, params: ?types.JSONValue) !types.JSONValue { + if (params == null or params.? != .object) { + return types.JSONValue{ .null = {} }; + } + + const params_obj = params.?.object; + const name = params_obj.get("name").?.string; + + if (self.handlers.get(name)) |handler| { + const arguments = params_obj.get("arguments"); + const result = try handler(arguments orelse .{ .object = std.json.ObjectMap.init(self.allocator) }, self.allocator); + return result; + } + + // Return error + var error_obj = std.json.ObjectMap.init(self.allocator); + try error_obj.put("content", .{ .array = std.json.Array.init(self.allocator) }); + try error_obj.put("isError", .{ .bool = true }); + + return .{ .object = error_obj }; + } + + fn handleResourcesList(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue { + var resources_array = std.json.Array.init(self.allocator); + for (self.resources.items) |resource| { + var res_obj = std.json.ObjectMap.init(self.allocator); + try res_obj.put("uri", .{ .string = resource.uri }); + try res_obj.put("name", .{ .string = resource.name }); + try res_obj.put("description", .{ .string = resource.description }); + try res_obj.put("mimeType", .{ .string = resource.mimeType }); + try resources_array.append(.{ .object = res_obj }); + } + + var result = std.json.ObjectMap.init(self.allocator); + try result.put("resources", .{ .array = resources_array }); + + return .{ .object = result }; + } + + fn handleResourcesRead(self: *MCPServer, params: ?types.JSONValue) !types.JSONValue { + if (params == null or params.? != .object) { + return types.JSONValue{ .null = {} }; + } + + const params_obj = params.?.object; + const uri = params_obj.get("uri").?.string; + + // Return empty content for now - implement actual resource reading + var contents_array = std.json.Array.init(self.allocator); + var result = std.json.ObjectMap.init(self.allocator); + try result.put("contents", .{ .array = contents_array }); + + return .{ .object = result }; + } + + fn handleResourceTemplatesList(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue { + var templates_array = std.json.Array.init(self.allocator); + + var result = std.json.ObjectMap.init(self.allocator); + try result.put("resourceTemplates", .{ .array = templates_array }); + + return .{ .object = result }; + } + + fn handlePromptsList(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue { + var prompts_array = std.json.Array.init(self.allocator); + for (self.prompts.items) |prompt| { + var prompt_obj = std.json.ObjectMap.init(self.allocator); + try prompt_obj.put("name", .{ .string = prompt.name }); + try prompt_obj.put("description", .{ .string = prompt.description }); + try prompts_array.append(.{ .object = prompt_obj }); + } + + var result = std.json.ObjectMap.init(self.allocator); + try result.put("prompts", .{ .array = prompts_array }); + + return .{ .object = result }; + } + + fn handlePromptsGet(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue { + return types.JSONValue{ .null = {} }; + } + + fn handleSampling(self: *MCPServer, _: ?types.JSONValue) !types.JSONValue { + return types.JSONValue{ .null = {} }; + } +}; + +fn jsonValueFromCapabilities(allocator: std.mem.Allocator, caps: types.ServerCapabilities) !types.JSONValue { + var obj = std.json.ObjectMap.init(allocator); + + if (caps.tools) |_| { + var tools_obj = std.json.ObjectMap.init(allocator); + try tools_obj.put("listChanged", .{ .bool = false }); + try obj.put("tools", .{ .object = tools_obj }); + } + + if (caps.resources) |_| { + var res_obj = std.json.ObjectMap.init(allocator); + try res_obj.put("subscribe", .{ .bool = false }); + try res_obj.put("listChanged", .{ .bool = false }); + try obj.put("resources", .{ .object = res_obj }); + } + + if (caps.prompts) |_| { + var prompts_obj = std.json.ObjectMap.init(allocator); + try prompts_obj.put("listChanged", .{ .bool = false }); + try obj.put("prompts", .{ .object = prompts_obj }); + } + + if (caps.sampling) |_| { + try obj.put("sampling", .{ .object = std.json.ObjectMap.init(allocator) }); + } + + return .{ .object = obj }; +} + +fn jsonValueFromServerInfo(allocator: std.mem.Allocator, info: types.ServerInfo) !types.JSONValue { + var obj = std.json.ObjectMap.init(allocator); + try obj.put("name", .{ .string = info.name }); + try obj.put("version", .{ .string = info.version }); + return .{ .object = obj }; +} diff --git a/src/mcp/transport.zig b/src/mcp/transport.zig new file mode 100644 index 0000000..9841ccc --- /dev/null +++ b/src/mcp/transport.zig @@ -0,0 +1,199 @@ +const std = @import("std"); +const types = @import("types.zig"); + +pub const Transport = union(enum) { + stdio: StdioTransport, + http: HttpTransport, +}; + +pub const StdioTransport = struct { + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator) StdioTransport { + return StdioTransport{ + .allocator = allocator, + }; + } + + pub fn readMessage(self: StdioTransport) ![]u8 { + // Read length as 4-byte big-endian integer + var len_buf: [4]u8 = undefined; + const bytes_read = try std.os.read(std.os.STDIN_FILENO, &len_buf); + if (bytes_read == 0) { + return error.ConnectionClosed; + } + if (bytes_read < 4) { + return error.InvalidMessage; + } + + const len = std.mem.readBig(u32, &len_buf); + if (len > 10 * 1024 * 1024) { + return error.MessageTooLarge; + } + + const message = try self.allocator.alloc(u8, len); + errdefer self.allocator.free(message); + + var remaining = len; + var offset: usize = 0; + while (remaining > 0) { + const read = try std.os.read(std.os.STDIN_FILENO, message[offset..]); + if (read == 0) { + return error.ConnectionClosed; + } + offset += read; + remaining -= read; + } + + return message; + } + + pub fn writeMessage(self: StdioTransport, data: []const u8) !void { + // Write length as 4-byte big-endian integer followed by data + var len_buf: [4]u8 = undefined; + std.mem.writeBig(u32, &len_buf, @intCast(data.len)); + + var total_written: usize = 0; + while (total_written < 4) { + const written = try std.os.write(std.os.STDOUT_FILENO, len_buf[total_written..]); + if (written == 0) { + return error.ConnectionClosed; + } + total_written += written; + } + + total_written = 0; + while (total_written < data.len) { + const written = try std.os.write(std.os.STDOUT_FILENO, data[total_written..]); + if (written == 0) { + return error.ConnectionClosed; + } + total_written += written; + } + } +}; + +pub const HttpTransport = struct { + allocator: std.mem.Allocator, + stream: std.net.Stream, + buffer: std.ArrayList(u8), + + pub fn init(allocator: std.mem.Allocator, stream: std.net.Stream) HttpTransport { + return HttpTransport{ + .allocator = allocator, + .stream = stream, + .buffer = std.ArrayList(u8).init(allocator), + }; + } + + pub fn deinit(self: *HttpTransport) void { + self.buffer.deinit(); + } + + pub fn readMessage(self: *HttpTransport) ![]u8 { + // Read HTTP headers + var header_buf = [4096]u8{}; + var header_end: usize = 0; + + while (true) { + const bytes_read = try self.stream.read(header_buf[header_end..header_end + 1]); + if (bytes_read == 0) { + return error.ConnectionClosed; + } + + if (header_end >= 3 and + header_buf[header_end - 3] == '\r' and + header_buf[header_end - 2] == '\n' and + header_buf[header_end - 1] == '\r' and + header_end >= 1 and header_buf[header_end] == '\n') { + break; + } + header_end += bytes_read; + } + + // Parse Content-Length header + const headers = header_buf[0..header_end]; + var content_length: usize = 0; + var lines = std.mem.split(u8, headers, "\r\n"); + while (lines.next()) |line| { + if (std.ascii.startsWithIgnoreCase(line, "content-length:")) { + const value = std.mem.trim(u8, line[15..], " "); + content_length = try std.fmt.parseInt(usize, value, 10); + } + } + + if (content_length == 0) { + return error.InvalidMessage; + } + + // Read body + const body = try self.allocator.alloc(u8, content_length); + errdefer self.allocator.free(body); + + var remaining = content_length; + var offset: usize = 0; + while (remaining > 0) { + const read = try self.stream.read(body[offset..]); + if (read == 0) { + return error.ConnectionClosed; + } + offset += read; + remaining -= read; + } + + return body; + } + + pub fn writeMessage(self: *HttpTransport, data: []const u8) !void { + // Write HTTP response with SSE + var buf = std.ArrayList(u8).init(self.allocator); + defer buf.deinit(); + + try buf.writer().print( + "HTTP/1.1 200 OK\r\n" ++ + "Content-Type: text/event-stream\r\n" ++ + "Cache-Control: no-cache\r\n" ++ + "Connection: keep-alive\r\n" ++ + "Transfer-Encoding: chunked\r\n" ++ + "\r\n", + .{} + ); + + // Write chunked body + try buf.writer().print("{x}\r\n", .{data.len}); + try buf.appendSlice(data); + try buf.appendSlice("\r\n"); + try buf.appendSlice("0\r\n\r\n"); + + try self.stream.writeAll(buf.items); + } +}; + +pub fn runStdioServer(allocator: std.mem.Allocator, server: anytype) !void { + var transport = StdioTransport.init(allocator); + + while (true) { + const message = transport.readMessage() catch |err| { + if (err == error.ConnectionClosed) { + break; + } + std.log.err("Failed to read message: {}", .{err}); + continue; + }; + + const response = server.handleMessage(message) catch |err| { + std.log.err("Failed to handle message: {}", .{err}); + allocator.free(message); + continue; + }; + allocator.free(message); + + if (response.len > 0) { + transport.writeMessage(response) catch |err| { + std.log.err("Failed to write response: {}", .{err}); + break; + }; + allocator.free(response); + } + } +} diff --git a/src/mcp/types.zig b/src/mcp/types.zig new file mode 100644 index 0000000..8bfe851 --- /dev/null +++ b/src/mcp/types.zig @@ -0,0 +1,431 @@ +const std = @import("std"); + +pub const JSONRPCVersion = "2.0"; + +pub const ErrorCode = enum(i32) { + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, +}; + +pub const JSONRPCError = struct { + code: ErrorCode, + message: []const u8, + data: ?[]const u8 = null, +}; + +pub const ProgressToken = union(enum) { + string: []const u8, + number: i64, +}; + +pub const JSONValue = union(enum) { + null, + bool: bool, + number: f64, + string: []const u8, + array: []JSONValue, + object: std.json.ObjectMap, +}; + +pub const TextContent = struct { + type: []const u8 = "text", + text: []const u8, +}; + +pub const ImageContent = struct { + type: []const u8 = "image", + data: []const u8, + mimeType: []const u8, +}; + +pub const BlobResourceContents = struct { + type: []const u8 = "blob", + blob: []const u8, + mimeType: []const u8, +}; + +pub const EmbeddedResource = struct { + type: []const u8 = "resource", + resource: Resource, +}; + +pub const Content = union(enum) { + text: TextContent, + image: ImageContent, + blob: BlobResourceContents, + embedded: EmbeddedResource, +}; + +pub const Resource = struct { + uri: []const u8, + mimeType: []const u8, + text: ?[]const u8 = null, + blob: ?[]const u8 = null, +}; + +pub const TextResourceContents = struct { + uri: []const u8, + mimeType: []const u8, + text: []const u8, +}; + +pub const CallToolResult = struct { + content: []Content, + isError: bool = false, +}; + +pub const Tool = struct { + name: []const u8, + description: []const u8, + inputSchema: std.json.ObjectMap, +}; + +pub const ResourceTemplate = struct { + uriTemplate: []const u8, + name: []const u8, + description: []const u8, + mimeType: []const u8, +}; + +pub const Resource_ = struct { + uri: []const u8, + name: []const u8, + description: []const u8, + mimeType: []const u8, +}; + +pub const Prompt = struct { + name: []const u8, + description: []const u8, + arguments: ?[]struct { + name: []const u8, + description: []const u8, + required: bool, + } = null, +}; + +pub const PromptMessage = struct { + role: []const u8, + content: Content, +}; + +pub const PromptResult = struct { + messages: []PromptMessage, +}; + +pub const SamplingMessage = struct { + role: []const u8, + content: Content, +}; + +pub const CreateMessageRequest = struct { + messages: []SamplingMessage, + systemPrompt: ?[]const u8 = null, + includeContext: []const u8, + temperature: ?f64 = null, + maxTokens: ?u32 = null, +}; + +pub const CreateMessageResult = struct { + role: []const u8, + content: Content, + model: []const u8, + stopReason: []const u8, +}; + +pub const InitializeResult = struct { + protocolVersion: []const u8, + capabilities: ServerCapabilities, + serverInfo: ServerInfo, +}; + +pub const ServerCapabilities = struct { + tools: ?struct { + listChanged: bool = false, + } = null, + resources: ?struct { + subscribe: bool = false, + listChanged: bool = false, + } = null, + prompts: ?struct { + listChanged: bool = false, + } = null, + sampling: ?struct {}, +}; + +pub const ServerInfo = struct { + name: []const u8, + version: []const u8, +}; + +pub const ClientCapabilities = struct { + tools: ?struct { + listChanged: bool = false, + } = null, + resources: ?struct { + subscribe: bool = false, + listChanged: bool = false, + } = null, + prompts: ?struct { + listChanged: bool = false, + } = null, + sampling: ?struct {}, +}; + +pub const InitializeParams = struct { + protocolVersion: []const u8, + capabilities: ClientCapabilities, + clientInfo: ClientInfo, +}; + +pub const ClientInfo = struct { + name: []const u8, + version: []const u8, +}; + +pub const EmptyParams = struct {}; + +pub const ListToolsResult = struct { + tools: []Tool, +}; + +pub const ListResourcesResult = struct { + resources: []Resource_, +}; + +pub const ListResourceTemplatesResult = struct { + resourceTemplates: []ResourceTemplate, +}; + +pub const ListPromptsResult = struct { + prompts: []Prompt, +}; + +pub const ReadResourceResult = struct { + contents: []const union(enum) { + TextResourceContents, + BlobResourceContents, + }, +}; + +pub const JSONRPCMessage = union(enum) { + request: JSONRPCRequest, + notification: JSONRPCNotification, + response: JSONRPCResponse, + error: JSONRPCErrorResponse, +}; + +pub const JSONRPCRequest = struct { + jsonrpc: []const u8, + id: union(enum) { + string: []const u8, + number: i64, + }, + method: []const u8, + params: ?JSONValue = null, +}; + +pub const JSONRPCNotification = struct { + jsonrpc: []const u8, + method: []const u8, + params: ?JSONValue = null, +}; + +pub const JSONRPCResponse = struct { + jsonrpc: []const u8, + id: union(enum) { + string: []const u8, + number: i64, + }, + result: JSONValue, +}; + +pub const JSONRPCErrorResponse = struct { + jsonrpc: []const u8, + id: ?union(enum) { + string: []const u8, + number: i64, + } = null, + error: JSONRPCError, +}; + +pub fn parseMessage(allocator: std.mem.Allocator, data: []const u8) !JSONRPCMessage { + const parsed = try std.json.parseFromSlice(JSONValue, allocator, data, .{}); + defer parsed.deinit(); + + const value = parsed.value; + if (value != .object) { + return error.InvalidJSON; + } + + const obj = value.object; + + // Check if it's a request (has method and id) + if (obj.get("method")) |method_val| { + if (obj.get("id")) |id_val| { + // Request + const method = method_val.string; + const id = if (id_val == .number) .{ number = @intCast(id_val.number) } else .{ string = id_val.string }; + const params = if (obj.get("params")) |p| p else null; + + return JSONRPCMessage{ + .request = .{ + .jsonrpc = JSONRPCVersion, + .id = id, + .method = method, + .params = params, + }, + }; + } else { + // Notification + const method = method_val.string; + const params = if (obj.get("params")) |p| p else null; + + return JSONRPCMessage{ + .notification = .{ + .jsonrpc = JSONRPCVersion, + .method = method, + .params = params, + }, + }; + } + } + + // Check if it's an error response + if (obj.get("error")) |_| { + const id = if (obj.get("id")) |id_val| if (id_val == .number) .{ number = @intCast(id_val.number) } else .{ string = id_val.string } else null; + const err_obj = obj.get("error").?.object; + const code = @enumFromInt(@intFromFloat(err_obj.get("code").?.number)); + const message = err_obj.get("message").?.string; + const data_val = if (err_obj.get("data")) |d| d.string else null; + + return JSONRPCMessage{ + .error = .{ + .jsonrpc = JSONRPCVersion, + .id = id, + .error = .{ .code = code, .message = message, .data = data_val }, + }, + }; + } + + // Must be a response + const id = if (obj.get("id").?) |id_val| if (id_val == .number) .{ number = @intCast(id_val.number) } else .{ string = id_val.string } else unreachable; + const result = obj.get("result").?; + + return JSONRPCMessage{ + .response = .{ + .jsonrpc = JSONRPCVersion, + .id = id, + .result = result, + }, + }; +} + +pub fn serializeMessage(allocator: std.mem.Allocator, msg: JSONRPCMessage) ![]u8 { + var buf = std.ArrayList(u8).init(allocator); + try serializeMessageWriter(msg, buf.writer()); + return buf.toOwnedSlice(); +} + +fn serializeMessageWriter(msg: JSONRPCMessage, writer: anytype) !void { + switch (msg) { + .request => |req| { + try writer.writeByte('{'); + try writer.print("\"jsonrpc\":\"{s}\",\"id\":", .{req.jsonrpc}); + switch (req.id) { + .string => |s| try writer.print("\"{s}\",", .{s}), + .number => |n| try writer.print("{},", .{n}), + } + try writer.print("\"method\":\"{s}\"", .{req.method}); + if (req.params) |params| { + try writer.writeByte(','); + try writer.writeAll("\"params\":"); + try jsonValueWrite(params, writer); + } + try writer.writeByte('}'); + }, + .notification => |notif| { + try writer.writeByte('{'); + try writer.print("\"jsonrpc\":\"{s}\",\"method\":\"{s}\"", .{notif.jsonrpc, notif.method}); + if (notif.params) |params| { + try writer.writeByte(','); + try writer.writeAll("\"params\":"); + try jsonValueWrite(params, writer); + } + try writer.writeByte('}'); + }, + .response => |resp| { + try writer.writeByte('{'); + try writer.print("\"jsonrpc\":\"{s}\",\"id\":", .{resp.jsonrpc}); + switch (resp.id) { + .string => |s| try writer.print("\"{s}\",", .{s}), + .number => |n| try writer.print("{},", .{n}), + } + try writer.writeAll("\"result\":"); + try jsonValueWrite(resp.result, writer); + try writer.writeByte('}'); + }, + .error => |err| { + try writer.writeByte('{'); + try writer.print("\"jsonrpc\":\"{s}\",\"error\":{{"code":{},\"message\":\"{s}\"", .{err.jsonrpc, @intFromEnum(err.error.code), err.error.message}); + if (err.error.data) |data| { + try writer.print(",\"data\":\"{s}\"", .{data}); + } + try writer.writeByte('}'); + if (err.id) |id| { + try writer.writeByte(','); + try writer.writeAll("\"id\":"); + switch (id) { + .string => |s| try writer.print("\"{s}\"", .{s}), + .number => |n| try writer.writeAll("{}*{n}"), + } + } + try writer.writeByte('}'); + }, + } +} + +fn jsonValueWrite(val: JSONValue, writer: anytype) !void { + switch (val) { + .null => try writer.writeAll("null"), + .bool => |b| try writer.writeAll(if (b) "true" else "false"), + .number => |n| try writer.print("{}", .{n}), + .string => |s| { + try writer.writeByte('"'); + for (s) |c| { + switch (c) { + '\\' => try writer.writeAll("\\\\"), + '"' => try writer.writeAll("\\\""), + '\n' => try writer.writeAll("\\n"), + '\r' => try writer.writeAll("\\r"), + '\t' => try writer.writeAll("\\t"), + else => try writer.writeByte(c), + } + } + try writer.writeByte('"'); + }, + .array => |arr| { + try writer.writeByte('['); + for (arr, 0..) |item, i| { + if (i > 0) try writer.writeByte(','); + try jsonValueWrite(item, writer); + } + try writer.writeByte(']'); + }, + .object => |obj| { + try writer.writeByte('{'); + var first = true; + var it = obj.iterator(); + while (it.next()) |entry| { + if (!first) try writer.writeByte(','); + first = false; + try writer.print("\"{s}\":", .{entry.key_ptr.*}); + try jsonValueWrite(entry.value_ptr.*, writer); + } + try writer.writeByte('}'); + }, + } +}