|
| 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 | +// arango-mcp/adapter/arango_mcp_adapter.zig -- Unified three-protocol adapter. |
| 5 | +// Replaces banned arango_mcp_adapter.v (V-lang, removed 2026-04-12). |
| 6 | +// REST:9181 gRPC:9182 GraphQL:9183 |
| 7 | +// Tools: arango_connect, arango_aql, arango_insert, arango_get... |
| 8 | + |
| 9 | +const std = @import("std"); |
| 10 | +const ffi = @import("arango_mcp_ffi"); |
| 11 | + |
| 12 | +const REST_PORT: u16 = 9181; |
| 13 | +const GRPC_PORT: u16 = 9182; |
| 14 | +const GQL_PORT: u16 = 9183; |
| 15 | +const MAX_CONN_BUF: usize = 16 * 1024; |
| 16 | + |
| 17 | +fn okJson(buf: []u8, msg: []const u8) []u8 { |
| 18 | + const n = std.fmt.bufPrint(buf, "{\"success\":true,\"message\":\"{s}\"}", .{msg}) catch return buf[0..0]; return n; |
| 19 | +} |
| 20 | +fn errJson(buf: []u8, msg: []const u8) []u8 { |
| 21 | + const n = std.fmt.bufPrint(buf, "{\"success\":false,\"error\":\"{s}\"}", .{msg}) catch return buf[0..0]; return n; |
| 22 | +} |
| 23 | +fn statusJson(buf: []u8) []u8 { |
| 24 | + const n = std.fmt.bufPrint(buf, "{\"success\":true,\"state\":\"ready\",\"service\":\"arango-mcp\"}", .{}) catch return buf[0..0]; return n; |
| 25 | +} |
| 26 | + |
| 27 | +const Response = struct { status: u16, body: []u8 }; |
| 28 | + |
| 29 | +fn dispatch(tool: []const u8, body: []const u8, resp: []u8) Response { |
| 30 | + _ = body; |
| 31 | + if (std.mem.eql(u8, tool, "arango_connect")) return .{ .status = 200, .body = okJson(resp, "arango_connect forwarded") }; |
| 32 | + if (std.mem.eql(u8, tool, "arango_aql")) return .{ .status = 200, .body = okJson(resp, "arango_aql forwarded") }; |
| 33 | + if (std.mem.eql(u8, tool, "arango_insert")) return .{ .status = 200, .body = okJson(resp, "arango_insert forwarded") }; |
| 34 | + if (std.mem.eql(u8, tool, "arango_get")) return .{ .status = 200, .body = okJson(resp, "arango_get forwarded") }; |
| 35 | + if (std.mem.eql(u8, tool, "arango_update")) return .{ .status = 200, .body = okJson(resp, "arango_update forwarded") }; |
| 36 | + if (std.mem.eql(u8, tool, "arango_delete")) return .{ .status = 200, .body = okJson(resp, "arango_delete forwarded") }; |
| 37 | + if (std.mem.eql(u8, tool, "arango_graph_traversal")) return .{ .status = 200, .body = okJson(resp, "arango_graph_traversal forwarded") }; |
| 38 | + if (std.mem.eql(u8, tool, "arango_list_collections")) return .{ .status = 200, .body = okJson(resp, "arango_list_collections forwarded") }; |
| 39 | + if (std.mem.eql(u8, tool, "arango_disconnect")) return .{ .status = 200, .body = okJson(resp, "arango_disconnect forwarded") }; |
| 40 | + if (std.mem.eql(u8, tool, "status") or std.mem.eql(u8, tool, "health")) return .{ .status = 200, .body = statusJson(resp) }; |
| 41 | + return .{ .status = 404, .body = errJson(resp, "Unknown tool") }; |
| 42 | +} |
| 43 | + |
| 44 | +fn dispatchRest(path: []const u8, body: []const u8, resp: []u8) Response { |
| 45 | + if (std.mem.startsWith(u8, path, "/tools/")) return dispatch(path["/tools/".len..], body, resp); |
| 46 | + if (std.mem.eql(u8, path, "/status") or std.mem.eql(u8, path, "/health")) return .{ .status = 200, .body = statusJson(resp) }; |
| 47 | + return .{ .status = 404, .body = errJson(resp, "Not found") }; |
| 48 | +} |
| 49 | + |
| 50 | +fn dispatchGrpc(path: []const u8, body: []const u8, resp: []u8) Response { |
| 51 | + const prefix = "/ArangoMcpservice/"; |
| 52 | + if (!std.mem.startsWith(u8, path, prefix)) return .{ .status = 404, .body = errJson(resp, "Not a recognized gRPC path") }; |
| 53 | + const method = path[prefix.len..]; |
| 54 | + const tool = blk: { |
| 55 | + if (std.mem.eql(u8, method, "arango_connect")) break :blk "arango_connect"; |
| 56 | + if (std.mem.eql(u8, method, "arango_aql")) break :blk "arango_aql"; |
| 57 | + if (std.mem.eql(u8, method, "arango_insert")) break :blk "arango_insert"; |
| 58 | + if (std.mem.eql(u8, method, "arango_get")) break :blk "arango_get"; |
| 59 | + if (std.mem.eql(u8, method, "arango_update")) break :blk "arango_update"; |
| 60 | + if (std.mem.eql(u8, method, "arango_delete")) break :blk "arango_delete"; |
| 61 | + if (std.mem.eql(u8, method, "arango_graph_traversal")) break :blk "arango_graph_traversal"; |
| 62 | + if (std.mem.eql(u8, method, "arango_list_collections")) break :blk "arango_list_collections"; |
| 63 | + if (std.mem.eql(u8, method, "arango_disconnect")) break :blk "arango_disconnect"; |
| 64 | + return .{ .status = 404, .body = errJson(resp, "Unknown gRPC method") }; |
| 65 | + }; |
| 66 | + return dispatch(tool, body, resp); |
| 67 | +} |
| 68 | + |
| 69 | +fn dispatchGraphql(body: []const u8, resp: []u8) Response { |
| 70 | + if (std.mem.indexOf(u8, body, "__schema") != null) return .{ .status = 200, .body = okJson(resp, "schema not supported") }; |
| 71 | + if (std.mem.indexOf(u8, body, "connect") != null) return dispatch("arango_connect", body, resp); |
| 72 | + if (std.mem.indexOf(u8, body, "aql") != null) return dispatch("arango_aql", body, resp); |
| 73 | + if (std.mem.indexOf(u8, body, "insert") != null) return dispatch("arango_insert", body, resp); |
| 74 | + if (std.mem.indexOf(u8, body, "get") != null) return dispatch("arango_get", body, resp); |
| 75 | + if (std.mem.indexOf(u8, body, "update") != null) return dispatch("arango_update", body, resp); |
| 76 | + if (std.mem.indexOf(u8, body, "delete") != null) return dispatch("arango_delete", body, resp); |
| 77 | + if (std.mem.indexOf(u8, body, "graph_traversal") != null) return dispatch("arango_graph_traversal", body, resp); |
| 78 | + if (std.mem.indexOf(u8, body, "list_collections") != null) return dispatch("arango_list_collections", body, resp); |
| 79 | + if (std.mem.indexOf(u8, body, "disconnect") != null) return dispatch("arango_disconnect", body, resp); |
| 80 | + return .{ .status = 200, .body = errJson(resp, "Unrecognised GraphQL operation") }; |
| 81 | +} |
| 82 | + |
| 83 | +const Protocol = enum { rest, grpc, graphql }; |
| 84 | + |
| 85 | +fn handleConnection(conn: std.net.Server.Connection, proto: Protocol) void { |
| 86 | + defer conn.stream.close(); |
| 87 | + var in_buf: [MAX_CONN_BUF]u8 = undefined; |
| 88 | + const n = conn.stream.read(&in_buf) catch return; |
| 89 | + const req = in_buf[0..n]; |
| 90 | + var path: []const u8 = "/"; var body: []const u8 = ""; |
| 91 | + if (n > 4) { |
| 92 | + const le = std.mem.indexOf(u8, req, "\r\n") orelse req.len; |
| 93 | + const fl = req[0..le]; const sp1 = std.mem.indexOfScalar(u8, fl, ' ') orelse 0; |
| 94 | + const ro = fl[sp1+1..]; const sp2 = std.mem.indexOfScalar(u8, ro, ' ') orelse ro.len; |
| 95 | + path = ro[0..sp2]; |
| 96 | + const bs = std.mem.indexOf(u8, req, "\r\n\r\n") orelse n; body = req[@min(bs+4,n)..]; |
| 97 | + } |
| 98 | + var rb: [MAX_CONN_BUF]u8 = undefined; |
| 99 | + const result = switch (proto) { .rest => dispatchRest(path, body, &rb), .grpc => dispatchGrpc(path, body, &rb), .graphql => dispatchGraphql(body, &rb) }; |
| 100 | + const ct: []const u8 = if (proto == .grpc) "application/grpc+json" else "application/json"; |
| 101 | + var hb: [256]u8 = undefined; |
| 102 | + const hdr = std.fmt.bufPrint(&hb, "HTTP/1.1 {d} OK\r\nContent-Type: {s}\r\nContent-Length: {d}\r\nConnection: close\r\n\r\n", .{ result.status, ct, result.body.len }) catch return; |
| 103 | + _ = conn.stream.writeAll(hdr) catch return; _ = conn.stream.writeAll(result.body) catch return; |
| 104 | +} |
| 105 | + |
| 106 | +fn listenLoop(port: u16, proto: Protocol) void { |
| 107 | + const addr = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, port) catch return; |
| 108 | + var srv = addr.listen(.{ .reuse_address = true }) catch return; defer srv.deinit(); |
| 109 | + while (true) { const conn = srv.accept() catch continue; handleConnection(conn, proto); } |
| 110 | +} |
| 111 | + |
| 112 | +pub fn main() !void { |
| 113 | + ffi.arango_mcp_init(); |
| 114 | + const t1 = try std.Thread.spawn(.{}, listenLoop, .{ REST_PORT, Protocol.rest }); |
| 115 | + const t2 = try std.Thread.spawn(.{}, listenLoop, .{ GRPC_PORT, Protocol.grpc }); |
| 116 | + const t3 = try std.Thread.spawn(.{}, listenLoop, .{ GQL_PORT, Protocol.graphql }); |
| 117 | + t1.join(); t2.join(); t3.join(); |
| 118 | +} |
0 commit comments