|
| 1 | +module YB = Yojson.Basic |
| 2 | + |
| 3 | +(** Basic MCP message types *) |
| 4 | +type mcp_request = |
| 5 | + { id : int; |
| 6 | + method_ : string; |
| 7 | + params : YB.t option |
| 8 | + } |
| 9 | + |
| 10 | +let parse_request json = |
| 11 | + let open YB.Util in |
| 12 | + { id = json |> member "id" |> to_int; |
| 13 | + method_ = json |> member "method" |> to_string; |
| 14 | + params = json |> member "params" |> to_option (fun x -> x) |
| 15 | + } |
| 16 | + |
| 17 | + |
| 18 | +type mcp_notif = { method_ : string } |
| 19 | + |
| 20 | +let parse_notif json = |
| 21 | + let open YB.Util in |
| 22 | + { method_ = json |> member "method" |> to_string } |
| 23 | + |
| 24 | + |
| 25 | +let json_of_result id result = |
| 26 | + `Assoc [ ("jsonrpc", `String "2.0"); ("id", `Int id); ("result", result) ] |
| 27 | + |
| 28 | + |
| 29 | +let json_of_error id code ?(data : string option) message = |
| 30 | + `Assoc |
| 31 | + [ ("jsonrpc", `String "2.0"); |
| 32 | + ("id", `Int id); |
| 33 | + ( "error", |
| 34 | + `Assoc |
| 35 | + ([ ("code", `Int code); ("message", `String message) ] |
| 36 | + @ match data with Some d -> [ ("data", `String d) ] | None -> []) ) |
| 37 | + ] |
| 38 | + |
| 39 | + |
| 40 | +(** Tools available via MCP *) |
| 41 | +let available_tools = |
| 42 | + `List |
| 43 | + [ `Assoc |
| 44 | + [ ("name", `String "wellFormedness"); |
| 45 | + ( "description", |
| 46 | + `String |
| 47 | + "Given a C file (and optionally a list of function names), runs CN's \ |
| 48 | + well-formedness check, which finds errors such as, ill-typing CN \ |
| 49 | + definitions (predicates, specifications, lemmas) and ill-formed recursion \ |
| 50 | + in datatypes. If given just a filename, all functions will be checked. \ |
| 51 | + Otherwise, only the functions listed will be checked. NOTE: this tool \ |
| 52 | + does not prove correctness, you need `verifier` for that." ); |
| 53 | + ( "inputSchema", |
| 54 | + `Assoc |
| 55 | + [ ("type", `String "object"); |
| 56 | + ( "properties", |
| 57 | + `Assoc |
| 58 | + [ ("filename", `Assoc [ ("type", `String "string") ]); |
| 59 | + ( "functions", |
| 60 | + `Assoc |
| 61 | + [ ("type", `String "array"); |
| 62 | + ("items", `Assoc [ ("type", `String "string") ]) |
| 63 | + ] ) |
| 64 | + ] ); |
| 65 | + ("required", `List [ `String "filename" ]) |
| 66 | + ] ); |
| 67 | + ( "annotations", |
| 68 | + `Assoc |
| 69 | + [ ("title", `String "CN well-formedness check"); |
| 70 | + ("destructiveHint", `Bool false); |
| 71 | + ("idempotentHint", `Bool true) |
| 72 | + ] ) |
| 73 | + ]; |
| 74 | + `Assoc |
| 75 | + [ ("name", `String "verifier"); |
| 76 | + ( "description", |
| 77 | + `String |
| 78 | + "Given a C file (and optionally a list of function names), use the CN \ |
| 79 | + verifier to try and prove correctness. If given just a filename, it will \ |
| 80 | + try to prove correctness of all functions not marked with `trusted`. \ |
| 81 | + Otherwise, only the functions listed will be checked." ); |
| 82 | + ( "inputSchema", |
| 83 | + `Assoc |
| 84 | + [ ("type", `String "object"); |
| 85 | + ( "properties", |
| 86 | + `Assoc |
| 87 | + [ ("filename", `Assoc [ ("type", `String "string") ]); |
| 88 | + ( "functions", |
| 89 | + `Assoc |
| 90 | + [ ("type", `String "array"); |
| 91 | + ("items", `Assoc [ ("type", `String "string") ]) |
| 92 | + ] ) |
| 93 | + ] ); |
| 94 | + ("required", `List [ `String "filename" ]) |
| 95 | + ] ); |
| 96 | + ( "annotations", |
| 97 | + `Assoc |
| 98 | + [ ("title", `String "CN verification"); |
| 99 | + ("destructiveHint", `Bool false); |
| 100 | + ("idempotentHint", `Bool true) |
| 101 | + ] ) |
| 102 | + ]; |
| 103 | + `Assoc |
| 104 | + [ ("name", `String "instrumentAndRun"); |
| 105 | + ( "description", |
| 106 | + `String |
| 107 | + "Given a C file (and optionally a list of function names), instrument the \ |
| 108 | + file to check CN specifications and run it. If given just a filename, all \ |
| 109 | + functions with specs will be have their specs checked at runtime. \ |
| 110 | + Otherwise, only the functions listed will have their specs checked at \ |
| 111 | + runtime. The file must have a `main` function for this to do anything." ); |
| 112 | + ( "inputSchema", |
| 113 | + `Assoc |
| 114 | + [ ("type", `String "object"); |
| 115 | + ( "properties", |
| 116 | + `Assoc |
| 117 | + [ ("filename", `Assoc [ ("type", `String "string") ]); |
| 118 | + ( "functions", |
| 119 | + `Assoc |
| 120 | + [ ("type", `String "array"); |
| 121 | + ("items", `Assoc [ ("type", `String "string") ]) |
| 122 | + ] ) |
| 123 | + ] ); |
| 124 | + ("required", `List [ `String "filename" ]) |
| 125 | + ] ); |
| 126 | + ( "annotations", |
| 127 | + `Assoc |
| 128 | + [ ("title", `String "Run with CN instrumentation"); |
| 129 | + ("destructiveHint", `Bool false); |
| 130 | + ("idempotentHint", `Bool false) |
| 131 | + ] ) |
| 132 | + ]; |
| 133 | + `Assoc |
| 134 | + [ ("name", `String "randomTesting"); |
| 135 | + ( "description", |
| 136 | + `String |
| 137 | + "Given a C file (and optionally a list of function names), use \ |
| 138 | + specification-based random testing. If given just a filename, as many \ |
| 139 | + functions as possible will be tested. Otherwise, only the functions \ |
| 140 | + listed will be tested." ); |
| 141 | + ( "inputSchema", |
| 142 | + `Assoc |
| 143 | + [ ("type", `String "object"); |
| 144 | + ( "properties", |
| 145 | + `Assoc |
| 146 | + [ ("filename", `Assoc [ ("type", `String "string") ]); |
| 147 | + ( "functions", |
| 148 | + `Assoc |
| 149 | + [ ("type", `String "array"); |
| 150 | + ("items", `Assoc [ ("type", `String "string") ]) |
| 151 | + ] ) |
| 152 | + ] ); |
| 153 | + ("required", `List [ `String "filename" ]) |
| 154 | + ] ); |
| 155 | + ( "annotations", |
| 156 | + `Assoc |
| 157 | + [ ("title", `String "CN random testing"); |
| 158 | + ("destructiveHint", `Bool false); |
| 159 | + ("idempotentHint", `Bool false) |
| 160 | + ] ) |
| 161 | + ] |
| 162 | + ] |
| 163 | + |
| 164 | + |
| 165 | +(** Handle different MCP methods *) |
| 166 | +let handle_request (req : mcp_request) = |
| 167 | + let open YB.Util in |
| 168 | + match req.method_ with |
| 169 | + | "ping" -> Lwt.return (json_of_result req.id (`Assoc [])) |
| 170 | + | "initialize" -> |
| 171 | + let result = |
| 172 | + `Assoc |
| 173 | + [ ("protocolVersion", `String "2025-03-26"); |
| 174 | + ("capabilities", `Assoc [ ("tools", `Assoc [ ("listChanged", `Bool false) ]) ]); |
| 175 | + ( "serverInfo", |
| 176 | + `Assoc |
| 177 | + [ ("name", `String "CN MCP Server"); |
| 178 | + ("version", `String Common.version_str) |
| 179 | + ] ) |
| 180 | + ] |
| 181 | + in |
| 182 | + Lwt.return (json_of_result req.id result) |
| 183 | + | "tools/list" -> |
| 184 | + let result = `Assoc [ ("tools", available_tools) ] in |
| 185 | + Lwt.return (json_of_result req.id result) |
| 186 | + | "tools/call" -> |
| 187 | + let toolName = req.params |> Option.get |> member "name" |> to_string in |
| 188 | + let args = req.params |> Option.get |> member "arguments" in |
| 189 | + (match toolName with |
| 190 | + | "wellFormedness" -> |
| 191 | + let cmd = |
| 192 | + "cn wf " |
| 193 | + ^ (args |> member "filename" |> to_string) |
| 194 | + ^ (match args |> member "functions" with |
| 195 | + | `List functions when not (List.is_empty functions) -> |
| 196 | + "--only=" ^ String.concat "," (List.map to_string functions) |
| 197 | + | _ -> "") |
| 198 | + ^ " 2>&1" |
| 199 | + in |
| 200 | + let inp = Unix.open_process_in cmd in |
| 201 | + let result = In_channel.input_all inp in |
| 202 | + In_channel.close inp; |
| 203 | + let resp = |
| 204 | + `Assoc |
| 205 | + [ ( "content", |
| 206 | + `List [ `Assoc [ ("type", `String "text"); ("text", `String result) ] ] ); |
| 207 | + ("isError", `Bool false) |
| 208 | + ] |
| 209 | + in |
| 210 | + Lwt.return (json_of_result req.id resp) |
| 211 | + | "verifier" -> |
| 212 | + let cmd = |
| 213 | + "cn verify " |
| 214 | + ^ (args |> member "filename" |> to_string) |
| 215 | + ^ (match args |> member "functions" with |
| 216 | + | `List functions when not (List.is_empty functions) -> |
| 217 | + "--only=" ^ String.concat "," (List.map to_string functions) |
| 218 | + | _ -> "") |
| 219 | + ^ " 2>&1" |
| 220 | + in |
| 221 | + let inp = Unix.open_process_in cmd in |
| 222 | + let result = In_channel.input_all inp in |
| 223 | + In_channel.close inp; |
| 224 | + let resp = |
| 225 | + `Assoc |
| 226 | + [ ( "content", |
| 227 | + `List [ `Assoc [ ("type", `String "text"); ("text", `String result) ] ] ); |
| 228 | + ("isError", `Bool false) |
| 229 | + ] |
| 230 | + in |
| 231 | + Lwt.return (json_of_result req.id resp) |
| 232 | + | "instrumentAndRun" -> |
| 233 | + let cmd = |
| 234 | + "cn instrument --run --tmp " |
| 235 | + ^ (args |> member "filename" |> to_string) |
| 236 | + ^ (match args |> member "functions" with |
| 237 | + | `List functions when not (List.is_empty functions) -> |
| 238 | + "--only=" ^ String.concat "," (List.map to_string functions) |
| 239 | + | _ -> "") |
| 240 | + ^ " 2>&1" |
| 241 | + in |
| 242 | + let inp = Unix.open_process_in cmd in |
| 243 | + let result = In_channel.input_all inp in |
| 244 | + In_channel.close inp; |
| 245 | + let resp = |
| 246 | + `Assoc |
| 247 | + [ ( "content", |
| 248 | + `List [ `Assoc [ ("type", `String "text"); ("text", `String result) ] ] ); |
| 249 | + ("isError", `Bool false) |
| 250 | + ] |
| 251 | + in |
| 252 | + Lwt.return (json_of_result req.id resp) |
| 253 | + | "randomTesting" -> |
| 254 | + let cmd = |
| 255 | + "cn test --progress-level=function " |
| 256 | + ^ (args |> member "filename" |> to_string) |
| 257 | + ^ (match args |> member "functions" with |
| 258 | + | `List functions when not (List.is_empty functions) -> |
| 259 | + "--only=" ^ String.concat "," (List.map to_string functions) |
| 260 | + | _ -> "") |
| 261 | + ^ " 2>&1" |
| 262 | + in |
| 263 | + let inp = Unix.open_process_in cmd in |
| 264 | + let result = In_channel.input_all inp in |
| 265 | + In_channel.close inp; |
| 266 | + let resp = |
| 267 | + `Assoc |
| 268 | + [ ( "content", |
| 269 | + `List [ `Assoc [ ("type", `String "text"); ("text", `String result) ] ] ); |
| 270 | + ("isError", `Bool false) |
| 271 | + ] |
| 272 | + in |
| 273 | + Lwt.return (json_of_result req.id resp) |
| 274 | + | toolName -> |
| 275 | + output_string |
| 276 | + stderr |
| 277 | + ("Invalid request: " ^ YB.pretty_to_string (Option.get req.params)); |
| 278 | + flush stderr; |
| 279 | + let err = json_of_error req.id (-32602) ("Unknown tool: " ^ toolName) in |
| 280 | + Lwt.return err) |
| 281 | + | _ -> |
| 282 | + let err = json_of_error req.id (-32601) "Method not found" in |
| 283 | + Lwt.return err |
| 284 | + |
| 285 | + |
| 286 | +(** Handle different MCP notifications *) |
| 287 | +let handle_notif (notif : mcp_notif) = match notif.method_ with _ -> () |
| 288 | + |
| 289 | +(** Start MCP server *) |
| 290 | +let mcp () = |
| 291 | + let rec main_loop () = |
| 292 | + let open Lwt.Infix in |
| 293 | + Lwt_io.read_line_opt Lwt_io.stdin |
| 294 | + >>= function |
| 295 | + | Some line -> |
| 296 | + (try |
| 297 | + let json = YB.from_string line in |
| 298 | + if YB.equal (YB.Util.member "id" json) `Null then ( |
| 299 | + (* Is a notification *) |
| 300 | + handle_notif (parse_notif json); |
| 301 | + main_loop ()) |
| 302 | + else ( |
| 303 | + let req = parse_request json in |
| 304 | + handle_request req |
| 305 | + >>= fun resp_json -> |
| 306 | + let resp_str = YB.to_string resp_json in |
| 307 | + Lwt_io.write_line Lwt_io.stdout resp_str |
| 308 | + >>= fun () -> Lwt_io.flush Lwt_io.stdout >>= fun () -> main_loop ()) |
| 309 | + with |
| 310 | + | Yojson.Json_error exn -> |
| 311 | + let err = json_of_error (-1) (-32700) ~data:exn "Parse error" in |
| 312 | + let err_str = YB.to_string err in |
| 313 | + Lwt_io.write_line Lwt_io.stdout err_str |
| 314 | + >>= fun () -> Lwt_io.flush Lwt_io.stdout >>= fun () -> main_loop ()) |
| 315 | + | None -> Lwt.return_unit |
| 316 | + in |
| 317 | + Lwt_main.run (main_loop ()) |
| 318 | + |
| 319 | + |
| 320 | +open Cmdliner |
| 321 | + |
| 322 | +module Flags = struct end |
| 323 | + |
| 324 | +let cmd = |
| 325 | + let open Term in |
| 326 | + let mcp_t = const mcp $ const () in |
| 327 | + let doc = |
| 328 | + "Starts a model context protocol (MCP) server exposing CN functionality. Offers \ |
| 329 | + tools for testing and verifying C functions with CN specifications." |
| 330 | + in |
| 331 | + let info = Cmd.info "mcp" ~doc in |
| 332 | + Cmd.v info mcp_t |
0 commit comments