Skip to content

Commit 1356469

Browse files
Refactor MCP server to call Goblint directly instead of via Server module
Remove the indirection through the Server module's JSON-RPC interface. The MCP server now: - Manages its own state (file, max_ids, arg_wrapper, invariant_parser, node_locator) - Calls Maingoblint.do_analyze and other Goblint functions directly - No longer depends on Server.t or converts between MCP and Server JSON-RPC This makes the architecture cleaner and more direct since the MCP server is built together with Goblint anyway. Co-authored-by: michael-schwarz <13812333+michael-schwarz@users.noreply.github.com>
1 parent b6acc14 commit 1356469

File tree

2 files changed

+158
-32
lines changed

2 files changed

+158
-32
lines changed

docs/developer-guide/mcp-architecture.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,16 @@ The MCP (Model Context Protocol) server provides a standardized way for LLMs and
1818
┌─────────────────┐
1919
│ mcpServer.ml │ MCP Protocol Layer
2020
│ - Tool defs │ - Handles MCP initialize/tools/call
21-
│ - Request │ - Converts MCP calls to Goblint ops
21+
│ - Request │ - Calls Goblint functionality directly
2222
│ handling │
2323
└────────┬────────┘
2424
2525
2626
┌─────────────────┐
27-
│ server.ml │ Existing Goblint Server
28-
│ - analyze() │ - Core analysis infrastructure
29-
│ - file parsing │ - Incremental analysis support
30-
│ - query APIs │
31-
└────────┬────────┘
32-
33-
34-
┌─────────────────┐
3527
│ Goblint Core │ Analysis Engine
36-
│ - CIL │
37-
│ - Solvers │
28+
│ - Maingoblint │ - Preprocessing and analysis
29+
│ - CIL │ - Configuration
30+
│ - Solvers │ - Query APIs
3831
│ - Analyses │
3932
└─────────────────┘
4033
```
@@ -47,8 +40,9 @@ The main MCP server implementation:
4740

4841
- **Protocol Types**: Defines MCP-specific types (ToolInput, ToolCall, ToolResult)
4942
- **Tool Definitions**: Specifies available tools with JSON schemas
50-
- **Request Handlers**: Processes MCP requests and dispatches to Goblint
43+
- **Request Handlers**: Processes MCP requests and calls Goblint functions directly
5144
- **I/O Loop**: Reads JSON-RPC from stdin, writes to stdout
45+
- **State Management**: Manages CIL file, ARG wrapper, invariant parser, and node locator
5246

5347
### goblint_mcp_server.ml
5448

@@ -76,8 +70,12 @@ Each tool follows this pattern:
7670
(* 2. Configure Goblint if needed *)
7771
GobConfig.set_* "option" value;
7872
79-
(* 3. Execute operation *)
80-
Server.analyze mcp_serv.server ~reset;
73+
(* 3. Execute operation directly *)
74+
Maingoblint.do_analyze None file;
75+
76+
(* 4. Return result *)
77+
ToolResult.make_text "Success message"
78+
```
8179

8280
(* 4. Return result *)
8381
ToolResult.make_text "Success message"
@@ -128,10 +126,12 @@ For production testing, integrate with actual MCP clients like Claude Desktop.
128126

129127
## Related Modules
130128

131-
- **Server** (src/util/server.ml): Existing JSON-RPC server infrastructure
132-
- **Maingoblint**: Entry points for analysis execution
129+
- **Maingoblint**: Entry points for preprocessing, parsing, and analysis execution
133130
- **GobConfig**: Configuration management
134131
- **Messages**: Analysis warning/error collection
132+
- **Cilfacade**: CIL interface and variable information
133+
- **ArgTools**: ARG (Abstract Reachability Graph) access
134+
- **WitnessUtil**: Invariant parsing and location lookup
135135

136136
## Dependencies
137137

src/util/mcpServer.ml

Lines changed: 142 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,137 @@ end
5757

5858
(** MCP Server state *)
5959
type t = {
60-
server: Server.t;
60+
mutable file: Cil.file option;
61+
mutable max_ids: MaxIdUtil.max_ids;
6162
mutable analyzed: bool;
63+
arg_wrapper: (module Server.ArgWrapper) ResettableLazy.t;
64+
invariant_parser: WitnessUtil.InvariantParser.t ResettableLazy.t;
65+
node_locator: WitnessUtil.Locator(Node).t ResettableLazy.t;
6266
}
6367

68+
(** Is node valid for lookup by location? *)
69+
let is_server_node cfgnode =
70+
let loc = UpdateCil.getLoc cfgnode in
71+
not loc.synthetic
72+
6473
let make () : t =
65-
(* Create server with stdin/stdout streams *)
66-
let server = Server.make None in
74+
(* Create arg_wrapper lazily *)
75+
let arg_wrapper = ResettableLazy.from_fun (fun () ->
76+
let module Arg = (val (Option.get_exn !ArgTools.current_arg (Failure "not analyzed or arg disabled"))) in
77+
let module Locator = WitnessUtil.Locator (Arg.Node) in
78+
let module StringH = Hashtbl.Make (Printable.Strings) in
79+
80+
let locator = Locator.create () in
81+
let ids = StringH.create 113 in
82+
let cfg_nodes = StringH.create 113 in
83+
Arg.iter_nodes (fun n ->
84+
let cfgnode = Arg.Node.cfgnode n in
85+
let loc = UpdateCil.getLoc cfgnode in
86+
if is_server_node cfgnode then
87+
Locator.add locator loc n;
88+
StringH.replace ids (Arg.Node.to_string n) n;
89+
StringH.add cfg_nodes (Node.show_id cfgnode) n
90+
);
91+
92+
let module ArgWrapper =
93+
struct
94+
module Arg = Arg
95+
module Locator = Locator
96+
let locator = locator
97+
let find_node = StringH.find ids
98+
let find_cfg_node = StringH.find_all cfg_nodes
99+
end
100+
in
101+
(module ArgWrapper: Server.ArgWrapper)
102+
)
103+
in
104+
105+
(* Create invariant_parser lazily *)
106+
let invariant_parser = ResettableLazy.from_fun (fun () ->
107+
WitnessUtil.InvariantParser.create !Cilfacade.current_file
108+
)
109+
in
110+
111+
(* Create node_locator lazily *)
112+
let node_locator = ResettableLazy.from_fun (fun () ->
113+
let module Cfg = (val !MyCFG.current_cfg) in
114+
let module Locator = WitnessUtil.Locator (Node) in
115+
let locator = Locator.create () in
116+
117+
(* DFS, copied from CfgTools.find_backwards_reachable *)
118+
let module NH = MyCFG.NodeH in
119+
let reachable = NH.create 100 in
120+
let rec iter_node node =
121+
if not (NH.mem reachable node) then begin
122+
NH.replace reachable node ();
123+
let loc = UpdateCil.getLoc node in
124+
if is_server_node node then
125+
Locator.add locator loc node;
126+
List.iter (fun (_, prev_node) ->
127+
iter_node prev_node
128+
) (Cfg.prev node)
129+
end
130+
in
131+
132+
Cil.iterGlobals !Cilfacade.current_file (function
133+
| GFun (fd, _) ->
134+
let return_node = Node.Function fd in
135+
iter_node return_node
136+
| _ -> ()
137+
);
138+
139+
locator
140+
)
141+
in
142+
67143
{
68-
server;
144+
file = None;
145+
max_ids = MaxIdUtil.get_file_max_ids Cil.dummyFile;
69146
analyzed = false;
147+
arg_wrapper;
148+
invariant_parser;
149+
node_locator;
70150
}
71151

152+
(** Analyze the configured files directly *)
153+
let analyze ?(reset=false) (mcp_serv: t) =
154+
Messages.Table.(MH.clear messages_table);
155+
Messages.(Table.MH.clear final_table);
156+
Messages.Table.messages_list := [];
157+
158+
(* Preprocess and parse files *)
159+
GoblintDir.init ();
160+
let file = Fun.protect ~finally:GoblintDir.finalize Maingoblint.preprocess_parse_merge in
161+
162+
if reset then (
163+
let max_ids = MaxIdUtil.get_file_max_ids file in
164+
mcp_serv.max_ids <- max_ids;
165+
Serialize.Cache.reset_data SolverData;
166+
Serialize.Cache.reset_data AnalysisData;
167+
);
168+
169+
(* Reset lazy modules *)
170+
ResettableLazy.reset mcp_serv.node_locator;
171+
ResettableLazy.reset mcp_serv.arg_wrapper;
172+
ResettableLazy.reset mcp_serv.invariant_parser;
173+
Cilfacade.reset_lazy ();
174+
InvariantCil.reset_lazy ();
175+
WideningThresholds.reset_lazy ();
176+
IntDomain.reset_lazy ();
177+
FloatDomain.reset_lazy ();
178+
StringDomain.reset_lazy ();
179+
PrecisionUtil.reset_lazy ();
180+
ApronDomain.reset_lazy ();
181+
AutoTune.reset_lazy ();
182+
LibraryFunctions.reset_lazy ();
183+
Access.reset ();
184+
185+
mcp_serv.file <- Some file;
186+
187+
(* Run analysis *)
188+
Maingoblint.do_analyze None file;
189+
Maingoblint.do_gobview file;
190+
72191
(** JSON Schema helpers *)
73192
let string_schema = `Assoc [("type", `String "string")]
74193
let bool_schema = `Assoc [("type", `String "boolean")]
@@ -284,8 +403,8 @@ let handle_tools_call (mcp_serv: t) (call: ToolCall.t) =
284403
(* Set the file to analyze *)
285404
GobConfig.set_list "files" [`String file];
286405

287-
(* Run analysis *)
288-
Server.analyze mcp_serv.server ~reset;
406+
(* Run analysis directly *)
407+
analyze mcp_serv ~reset;
289408
mcp_serv.analyzed <- true;
290409

291410
let status = if !AnalysisState.verified = Some false then "VerifyError" else "Success" in
@@ -302,8 +421,8 @@ let handle_tools_call (mcp_serv: t) (call: ToolCall.t) =
302421
(* Set compilation database *)
303422
GobConfig.set_string "pre.compdb.file" comp_db;
304423

305-
(* Run analysis *)
306-
Server.analyze mcp_serv.server ~reset;
424+
(* Run analysis directly *)
425+
analyze mcp_serv ~reset;
307426
mcp_serv.analyzed <- true;
308427

309428
let status = if !AnalysisState.verified = Some false then "VerifyError" else "Success" in
@@ -320,11 +439,17 @@ let handle_tools_call (mcp_serv: t) (call: ToolCall.t) =
320439
ToolResult.make_text (Yojson.Safe.to_string files_json)
321440

322441
| "get_functions" ->
323-
begin match mcp_serv.server.file with
442+
begin match mcp_serv.file with
324443
| Some file ->
325-
let functions = Server.Function.getFunctionsList file.globals in
326-
let json = `List (List.map Server.Function.to_yojson functions) in
327-
ToolResult.make_text (Yojson.Safe.to_string json)
444+
let functions = List.filter_map (function
445+
| Cil.GFun (fd, loc) -> Some `Assoc [
446+
("funName", `String fd.svar.vname);
447+
("location", CilType.Location.to_yojson loc)
448+
]
449+
| _ -> None
450+
) file.globals
451+
in
452+
ToolResult.make_text (Yojson.Safe.to_string (`List functions))
328453
| None ->
329454
ToolResult.make_error "No file has been analyzed yet"
330455
end
@@ -380,10 +505,11 @@ let handle_tools_call (mcp_serv: t) (call: ToolCall.t) =
380505
| Some node_id, None ->
381506
Node.of_id node_id
382507
| None, Some location ->
508+
let module Locator = WitnessUtil.Locator (Node) in
383509
let node_opt =
384510
let open GobOption.Syntax in
385-
let* nodes = Server.Locator.find_opt (ResettableLazy.force Server.node_locator) location in
386-
Server.Locator.ES.choose_opt nodes
511+
let* nodes = Locator.find_opt (ResettableLazy.force mcp_serv.node_locator) location in
512+
Locator.ES.choose_opt nodes
387513
in
388514
Option.get_exn node_opt (Failure "cannot find node for location")
389515
| _, _ ->
@@ -490,7 +616,7 @@ let handle_tools_call (mcp_serv: t) (call: ToolCall.t) =
490616
let cfg_node = Arg.Node.cfgnode n in
491617
let fundec = Node.find_fundec cfg_node in
492618
let loc = UpdateCil.getLoc cfg_node in
493-
begin match WitnessUtil.InvariantParser.parse_cil (ResettableLazy.force mcp_serv.server.invariant_parser) ~fundec ~loc exp_cabs with
619+
begin match WitnessUtil.InvariantParser.parse_cil (ResettableLazy.force mcp_serv.invariant_parser) ~fundec ~loc exp_cabs with
494620
| Ok exp -> exp
495621
| Error _ -> raise (Failure "CIL couldn't parse expression")
496622
end
@@ -516,7 +642,7 @@ let handle_tools_call (mcp_serv: t) (call: ToolCall.t) =
516642
let cfg_node = Arg.Node.cfgnode n in
517643
let fundec = Node.find_fundec cfg_node in
518644
let loc = UpdateCil.getLoc cfg_node in
519-
begin match WitnessUtil.InvariantParser.parse_cil (ResettableLazy.force mcp_serv.server.invariant_parser) ~fundec ~loc exp_cabs with
645+
begin match WitnessUtil.InvariantParser.parse_cil (ResettableLazy.force mcp_serv.invariant_parser) ~fundec ~loc exp_cabs with
520646
| Ok exp ->
521647
let x = Arg.query n (Queries.EvalInt exp) in
522648
let result = `Assoc [

0 commit comments

Comments
 (0)