|
| 1 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | +// |
| 4 | +// affinescript-mcp/adapter/affinescript_adapter.zig -- Unified three-protocol adapter. |
| 5 | +// |
| 6 | +// Replaces the banned affinescript_adapter.v (V-lang, removed 2026-04-12). |
| 7 | +// |
| 8 | +// Bridges the Zig FFI (affinescript_mcp_ffi.zig) to three network protocols: |
| 9 | +// REST :9031 POST /tools/<tool> |
| 10 | +// gRPC-compat :9032 /AffinescriptMcpService/<Method> |
| 11 | +// GraphQL :9033 POST /graphql { query: "..." } |
| 12 | +// |
| 13 | +// AffineScript compiler tools: type-check, parse, format, explain errors, compile, hover, complete, goto-def |
| 14 | +// Tools: |
| 15 | +// affinescript_check |
| 16 | +// affinescript_parse |
| 17 | +// affinescript_format |
| 18 | +// affinescript_explain_error |
| 19 | +// affinescript_stdlib |
| 20 | +// affinescript_syntax_ref |
| 21 | +// affinescript_snippet |
| 22 | +// affinescript_lint |
| 23 | +// affinescript_compile |
| 24 | +// affinescript_hover |
| 25 | +// affinescript_goto_def |
| 26 | +// affinescript_complete |
| 27 | + |
| 28 | +const std = @import("std"); |
| 29 | +const ffi = @import("affinescript_mcp_ffi"); |
| 30 | + |
| 31 | +const REST_PORT: u16 = 9031; |
| 32 | +const GRPC_PORT: u16 = 9032; |
| 33 | +const GQL_PORT: u16 = 9033; |
| 34 | + |
| 35 | +const MAX_CONN_BUF: usize = 16 * 1024; |
| 36 | + |
| 37 | +// ============================================================================ |
| 38 | +// JSON response builders |
| 39 | +// ============================================================================ |
| 40 | + |
| 41 | +fn okJson(buf: []u8, msg: []const u8) []u8 { |
| 42 | + return std.fmt.bufPrint(buf, |
| 43 | + \{{"success":true,"message":"{}"}} |
| 44 | + , .{msg}) catch buf[0..0]; |
| 45 | +} |
| 46 | + |
| 47 | +fn errJson(buf: []u8, msg: []const u8) []u8 { |
| 48 | + return std.fmt.bufPrint(buf, |
| 49 | + \{{"success":false,"error":"{}"}} |
| 50 | + , .{msg}) catch buf[0..0]; |
| 51 | +} |
| 52 | + |
| 53 | +fn statusJson(buf: []u8) []u8 { |
| 54 | + return std.fmt.bufPrint(buf, |
| 55 | + \{{"success":true,"state":"ready","service":"affinescript-mcp"}} |
| 56 | + , .{}) catch buf[0..0]; |
| 57 | +} |
| 58 | + |
| 59 | +// ============================================================================ |
| 60 | +// Tool dispatcher |
| 61 | +// ============================================================================ |
| 62 | + |
| 63 | +const Response = struct { status: u16, body: []u8 }; |
| 64 | + |
| 65 | +fn dispatch(tool: []const u8, body: []const u8, resp: []u8) Response { |
| 66 | + _ = body; |
| 67 | + if (std.mem.eql(u8, tool, "affinescript_check")) return .{ .status = 200, .body = okJson(resp, "affinescript_check forwarded to backend") }; |
| 68 | + if (std.mem.eql(u8, tool, "affinescript_parse")) return .{ .status = 200, .body = okJson(resp, "affinescript_parse forwarded to backend") }; |
| 69 | + if (std.mem.eql(u8, tool, "affinescript_format")) return .{ .status = 200, .body = okJson(resp, "affinescript_format forwarded to backend") }; |
| 70 | + if (std.mem.eql(u8, tool, "affinescript_explain_error")) return .{ .status = 200, .body = okJson(resp, "affinescript_explain_error forwarded to backend") }; |
| 71 | + if (std.mem.eql(u8, tool, "affinescript_stdlib")) return .{ .status = 200, .body = okJson(resp, "affinescript_stdlib forwarded to backend") }; |
| 72 | + if (std.mem.eql(u8, tool, "affinescript_syntax_ref")) return .{ .status = 200, .body = okJson(resp, "affinescript_syntax_ref forwarded to backend") }; |
| 73 | + if (std.mem.eql(u8, tool, "affinescript_snippet")) return .{ .status = 200, .body = okJson(resp, "affinescript_snippet forwarded to backend") }; |
| 74 | + if (std.mem.eql(u8, tool, "affinescript_lint")) return .{ .status = 200, .body = okJson(resp, "affinescript_lint forwarded to backend") }; |
| 75 | + if (std.mem.eql(u8, tool, "affinescript_compile")) return .{ .status = 200, .body = okJson(resp, "affinescript_compile forwarded to backend") }; |
| 76 | + if (std.mem.eql(u8, tool, "affinescript_hover")) return .{ .status = 200, .body = okJson(resp, "affinescript_hover forwarded to backend") }; |
| 77 | + if (std.mem.eql(u8, tool, "affinescript_goto_def")) return .{ .status = 200, .body = okJson(resp, "affinescript_goto_def forwarded to backend") }; |
| 78 | + if (std.mem.eql(u8, tool, "affinescript_complete")) return .{ .status = 200, .body = okJson(resp, "affinescript_complete forwarded to backend") }; |
| 79 | + if (std.mem.eql(u8, tool, "status") or std.mem.eql(u8, tool, "health")) |
| 80 | + return .{ .status = 200, .body = statusJson(resp) }; |
| 81 | + return .{ .status = 404, .body = errJson(resp, "Unknown tool") }; |
| 82 | +} |
| 83 | + |
| 84 | +// ============================================================================ |
| 85 | +// REST handler |
| 86 | +// ============================================================================ |
| 87 | + |
| 88 | +fn dispatchRest(path: []const u8, body: []const u8, resp: []u8) Response { |
| 89 | + if (std.mem.startsWith(u8, path, "/tools/")) { |
| 90 | + return dispatch(path["/tools/".len..], body, resp); |
| 91 | + } |
| 92 | + if (std.mem.eql(u8, path, "/status") or std.mem.eql(u8, path, "/health")) { |
| 93 | + return .{ .status = 200, .body = statusJson(resp) }; |
| 94 | + } |
| 95 | + return .{ .status = 404, .body = errJson(resp, "Not found") }; |
| 96 | +} |
| 97 | + |
| 98 | +// ============================================================================ |
| 99 | +// gRPC-compat handler |
| 100 | +// ============================================================================ |
| 101 | + |
| 102 | +fn dispatchGrpc(path: []const u8, body: []const u8, resp: []u8) Response { |
| 103 | + const prefix = "/AffinescriptMcpService/"; |
| 104 | + if (!std.mem.startsWith(u8, path, prefix)) |
| 105 | + return .{ .status = 404, .body = errJson(resp, "Not a recognized gRPC path") }; |
| 106 | + const method = path[prefix.len..]; |
| 107 | + const tool = blk: { |
| 108 | + if (std.mem.eql(u8, method, "AffinescriptCheck")) break :blk "affinescript_check"; |
| 109 | + if (std.mem.eql(u8, method, "AffinescriptParse")) break :blk "affinescript_parse"; |
| 110 | + if (std.mem.eql(u8, method, "AffinescriptFormat")) break :blk "affinescript_format"; |
| 111 | + if (std.mem.eql(u8, method, "AffinescriptExplainError")) break :blk "affinescript_explain_error"; |
| 112 | + if (std.mem.eql(u8, method, "AffinescriptStdlib")) break :blk "affinescript_stdlib"; |
| 113 | + if (std.mem.eql(u8, method, "AffinescriptSyntaxRef")) break :blk "affinescript_syntax_ref"; |
| 114 | + if (std.mem.eql(u8, method, "AffinescriptSnippet")) break :blk "affinescript_snippet"; |
| 115 | + if (std.mem.eql(u8, method, "AffinescriptLint")) break :blk "affinescript_lint"; |
| 116 | + if (std.mem.eql(u8, method, "AffinescriptCompile")) break :blk "affinescript_compile"; |
| 117 | + if (std.mem.eql(u8, method, "AffinescriptHover")) break :blk "affinescript_hover"; |
| 118 | + if (std.mem.eql(u8, method, "AffinescriptGotoDef")) break :blk "affinescript_goto_def"; |
| 119 | + if (std.mem.eql(u8, method, "AffinescriptComplete")) break :blk "affinescript_complete"; |
| 120 | + return .{ .status = 404, .body = errJson(resp, "Unknown gRPC method") }; |
| 121 | + }; |
| 122 | + return dispatch(tool, body, resp); |
| 123 | +} |
| 124 | + |
| 125 | +// ============================================================================ |
| 126 | +// GraphQL handler |
| 127 | +// ============================================================================ |
| 128 | + |
| 129 | +fn dispatchGraphql(body: []const u8, resp: []u8) Response { |
| 130 | + if (std.mem.indexOf(u8, body, "__schema") != null) |
| 131 | + return .{ .status = 200, .body = okJson(resp, "schema introspection not yet supported") }; |
| 132 | + if (std.mem.indexOf(u8, body, "check") != null) return dispatch("affinescript_check", body, resp); |
| 133 | + if (std.mem.indexOf(u8, body, "parse") != null) return dispatch("affinescript_parse", body, resp); |
| 134 | + if (std.mem.indexOf(u8, body, "format") != null) return dispatch("affinescript_format", body, resp); |
| 135 | + if (std.mem.indexOf(u8, body, "explain_error") != null) return dispatch("affinescript_explain_error", body, resp); |
| 136 | + if (std.mem.indexOf(u8, body, "stdlib") != null) return dispatch("affinescript_stdlib", body, resp); |
| 137 | + if (std.mem.indexOf(u8, body, "syntax_ref") != null) return dispatch("affinescript_syntax_ref", body, resp); |
| 138 | + if (std.mem.indexOf(u8, body, "snippet") != null) return dispatch("affinescript_snippet", body, resp); |
| 139 | + if (std.mem.indexOf(u8, body, "lint") != null) return dispatch("affinescript_lint", body, resp); |
| 140 | + if (std.mem.indexOf(u8, body, "compile") != null) return dispatch("affinescript_compile", body, resp); |
| 141 | + if (std.mem.indexOf(u8, body, "hover") != null) return dispatch("affinescript_hover", body, resp); |
| 142 | + if (std.mem.indexOf(u8, body, "goto_def") != null) return dispatch("affinescript_goto_def", body, resp); |
| 143 | + if (std.mem.indexOf(u8, body, "complete") != null) return dispatch("affinescript_complete", body, resp); |
| 144 | + return .{ .status = 200, .body = errJson(resp, "Unrecognised GraphQL operation") }; |
| 145 | +} |
| 146 | + |
| 147 | +// ============================================================================ |
| 148 | +// HTTP/1.1 connection handler |
| 149 | +// ============================================================================ |
| 150 | + |
| 151 | +const Protocol = enum { rest, grpc, graphql }; |
| 152 | + |
| 153 | +fn handleConnection(conn: std.net.Server.Connection, proto: Protocol) void { |
| 154 | + defer conn.stream.close(); |
| 155 | + var in_buf: [MAX_CONN_BUF]u8 = undefined; |
| 156 | + const n = conn.stream.read(&in_buf) catch return; |
| 157 | + const req = in_buf[0..n]; |
| 158 | + |
| 159 | + var path: []const u8 = "/"; |
| 160 | + var body: []const u8 = ""; |
| 161 | + if (n > 4) { |
| 162 | + const line_end = std.mem.indexOf(u8, req, "\r\n") orelse req.len; |
| 163 | + const first_line = req[0..line_end]; |
| 164 | + const sp1 = std.mem.indexOfScalar(u8, first_line, ' ') orelse 0; |
| 165 | + const rest_of = first_line[sp1 + 1 ..]; |
| 166 | + const sp2 = std.mem.indexOfScalar(u8, rest_of, ' ') orelse rest_of.len; |
| 167 | + path = rest_of[0..sp2]; |
| 168 | + const body_sep = std.mem.indexOf(u8, req, "\r\n\r\n") orelse n; |
| 169 | + body = req[@min(body_sep + 4, n)..]; |
| 170 | + } |
| 171 | + |
| 172 | + var resp_buf: [MAX_CONN_BUF]u8 = undefined; |
| 173 | + const result = switch (proto) { |
| 174 | + .rest => dispatchRest(path, body, &resp_buf), |
| 175 | + .grpc => dispatchGrpc(path, body, &resp_buf), |
| 176 | + .graphql => dispatchGraphql(body, &resp_buf), |
| 177 | + }; |
| 178 | + |
| 179 | + const content_type = switch (proto) { |
| 180 | + .rest => "application/json", |
| 181 | + .grpc => "application/grpc+json", |
| 182 | + .graphql => "application/json", |
| 183 | + }; |
| 184 | + |
| 185 | + var hdr_buf: [256]u8 = undefined; |
| 186 | + const hdr = std.fmt.bufPrint(&hdr_buf, |
| 187 | + "HTTP/1.1 {d} OK\r\nContent-Type: {s}\r\nContent-Length: {d}\r\nConnection: close\r\n\r\n", |
| 188 | + .{ result.status, content_type, result.body.len }, |
| 189 | + ) catch return; |
| 190 | + _ = conn.stream.writeAll(hdr) catch return; |
| 191 | + _ = conn.stream.writeAll(result.body) catch return; |
| 192 | +} |
| 193 | + |
| 194 | +fn listenLoop(port: u16, proto: Protocol) void { |
| 195 | + const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, port) catch return; |
| 196 | + var server = addr.listen(.{ .reuse_address = true }) catch return; |
| 197 | + defer server.deinit(); |
| 198 | + while (true) { |
| 199 | + const conn = server.accept() catch continue; |
| 200 | + handleConnection(conn, proto); |
| 201 | + } |
| 202 | +} |
| 203 | + |
| 204 | +pub fn main() !void { |
| 205 | + ffi.affinescript_init(); |
| 206 | + const rest_thread = try std.Thread.spawn(.{}, listenLoop, .{ REST_PORT, Protocol.rest }); |
| 207 | + const grpc_thread = try std.Thread.spawn(.{}, listenLoop, .{ GRPC_PORT, Protocol.grpc }); |
| 208 | + const gql_thread = try std.Thread.spawn(.{}, listenLoop, .{ GQL_PORT, Protocol.graphql }); |
| 209 | + rest_thread.join(); |
| 210 | + grpc_thread.join(); |
| 211 | + gql_thread.join(); |
| 212 | +} |
0 commit comments