OCaml Claude SDK using Eio and Jsont
1# Claude OCaml SDK Architecture (v2)
2
3This document describes the rearchitected OCaml SDK, aligned with the Python Claude Agent SDK while maintaining idiomatic OCaml/Eio patterns.
4
5## Core Design Principles
6
7### 1. MCP for Custom Tools (Python SDK Pattern)
8
9The Python SDK's key insight: **built-in tools are handled by Claude CLI, custom tools via MCP**.
10
11```
12Claude CLI SDK (OCaml Process)
13 | |
14 |--[tool_use Read]--->| (CLI handles internally)
15 | |
16 |<--[tool_result]----|
17 | |
18 |--[mcp_request]----->| (SDK handles via in-process MCP)
19 |<--[mcp_response]----|
20```
21
22This eliminates the current problem where the OCaml SDK intercepts ALL tool_use events.
23
24### 2. Hooks Intercept, Don't Execute
25
26Hooks provide interception points for **observation and control**, not execution:
27- PreToolUse: Allow/Deny/Modify before execution (by CLI or MCP)
28- PostToolUse: Observe/Modify after execution
29- Other lifecycle hooks remain the same
30
31### 3. Two-Level API
32
33Like Python, provide both simple and advanced interfaces:
34- `Claude.query`: One-shot queries, simple async iterator
35- `Claude.Client`: Full bidirectional, multi-turn, custom tools
36
37---
38
39## Module Structure
40
41```
42lib/
43├── claude.ml # Main module, re-exports public API
44├── claude.mli
45│
46├── client.ml # Bidirectional client
47├── client.mli
48│
49├── tool.ml # Custom tool definition
50├── tool.mli
51│
52├── mcp_server.ml # In-process MCP server
53├── mcp_server.mli
54│
55├── hook.ml # Hook types and matchers
56├── hook.mli
57│
58├── options.ml # Configuration
59├── options.mli
60│
61├── message.ml # Message types
62├── message.mli
63│
64├── response.ml # Response events
65├── response.mli
66│
67├── model.ml # Model identifiers
68├── model.mli
69│
70├── permission_mode.ml # Permission modes
71├── permission_mode.mli
72│
73├── server_info.ml # Server metadata
74├── server_info.mli
75│
76├── err.ml # Error types
77├── err.mli
78│
79└── internal/
80 ├── process.ml # CLI process management
81 ├── protocol.ml # JSON wire protocol
82 └── mcp_handler.ml # MCP message routing
83```
84
85---
86
87## Core Types
88
89### Tool Definition
90
91```ocaml
92(* tool.mli *)
93
94(** Custom tool for MCP servers.
95
96 Tools are functions that Claude can invoke. They run in-process
97 within your OCaml application via MCP protocol.
98
99 {[
100 let greet = Tool.create
101 ~name:"greet"
102 ~description:"Greet a user by name"
103 ~input_schema:(`O ["name", `String "string"])
104 ~handler:(fun args ->
105 match Jsont.find_string "name" args with
106 | Some name -> Ok (`String (Printf.sprintf "Hello, %s!" name))
107 | None -> Error "Missing 'name' parameter")
108 ]} *)
109
110type t
111
112val create :
113 name:string ->
114 description:string ->
115 input_schema:Jsont.json ->
116 handler:(Jsont.json -> (Jsont.json, string) result) ->
117 t
118(** [create ~name ~description ~input_schema ~handler] creates a custom tool.
119
120 @param name Unique tool identifier. Claude uses this in function calls.
121 @param description Human-readable description for Claude.
122 @param input_schema JSON Schema for input validation.
123 @param handler Function that executes the tool and returns result or error. *)
124
125val name : t -> string
126val description : t -> string
127val input_schema : t -> Jsont.json
128val call : t -> Jsont.json -> (Jsont.json, string) result
129
130(** {2 Async Tools}
131
132 For tools that need Eio concurrency: *)
133
134type async_t
135
136val create_async :
137 name:string ->
138 description:string ->
139 input_schema:Jsont.json ->
140 handler:(sw:Eio.Switch.t -> Jsont.json -> (Jsont.json, string) result) ->
141 async_t
142(** Create a tool that runs under an Eio switch for async operations. *)
143```
144
145### MCP Server
146
147```ocaml
148(* mcp_server.mli *)
149
150(** In-process MCP server.
151
152 SDK MCP servers run directly in your OCaml application, eliminating
153 subprocess overhead. They handle tools/list and tools/call requests.
154
155 {[
156 let server = Mcp_server.create
157 ~name:"my-tools"
158 ~tools:[greet_tool; calculate_tool]
159 ()
160
161 let options = Options.default
162 |> Options.with_mcp_server ~name:"tools" server
163 |> Options.with_allowed_tools ["mcp__tools__greet"]
164 ]} *)
165
166type t
167
168val create :
169 name:string ->
170 ?version:string ->
171 tools:Tool.t list ->
172 unit ->
173 t
174(** [create ~name ?version ~tools ()] creates an in-process MCP server.
175
176 @param name Server identifier. Tools are accessed as [mcp__<name>__<tool>].
177 @param version Server version (default "1.0.0").
178 @param tools List of tools this server provides. *)
179
180val name : t -> string
181val version : t -> string
182val tools : t -> Tool.t list
183
184(** {2 MCP Protocol Handling} *)
185
186val handle_request :
187 t ->
188 method_:string ->
189 params:Jsont.json ->
190 (Jsont.json, string) result
191(** [handle_request t ~method_ ~params] handles MCP JSONRPC requests.
192
193 Supports:
194 - [initialize]: Returns server capabilities
195 - [tools/list]: Returns available tools
196 - [tools/call]: Executes a tool *)
197```
198
199### Hooks
200
201```ocaml
202(* hook.mli *)
203
204(** Hook callbacks for event interception.
205
206 Hooks intercept events in the Claude agent loop. They can observe,
207 allow, deny, or modify tool execution.
208
209 {b Key difference from tool execution}: Hooks don't execute built-in
210 tools - Claude CLI handles those. Hooks only intercept for control.
211
212 {[
213 let block_rm = Hook.PreToolUse.handler (fun input ->
214 if input.tool_name = "Bash" then
215 match Tool_input.get_string input.tool_input "command" with
216 | Some cmd when String.is_substring cmd ~substring:"rm -rf" ->
217 Hook.PreToolUse.deny ~reason:"Dangerous command"
218 | _ -> Hook.PreToolUse.allow ()
219 else Hook.PreToolUse.allow ())
220
221 let hooks = Hook.Config.empty
222 |> Hook.Config.on_pre_tool_use ~pattern:"Bash" block_rm
223 ]} *)
224
225type context = {
226 session_id : string option;
227 transcript_path : string option;
228}
229(** Context provided to all hooks. *)
230
231(** {1 PreToolUse Hook}
232
233 Fires before any tool execution (built-in or MCP). *)
234module PreToolUse : sig
235 type input = {
236 tool_name : string;
237 tool_input : Tool_input.t;
238 context : context;
239 }
240
241 type decision =
242 | Allow
243 | Deny of { reason : string }
244 | Modify of { input : Tool_input.t }
245 | Ask of { reason : string option }
246
247 val allow : ?updated_input:Tool_input.t -> unit -> decision
248 val deny : reason:string -> decision
249 val ask : ?reason:string -> unit -> decision
250 val modify : input:Tool_input.t -> decision
251
252 type handler = input -> decision
253
254 val handler : (input -> decision) -> handler
255end
256
257(** {1 PostToolUse Hook}
258
259 Fires after tool execution completes. *)
260module PostToolUse : sig
261 type input = {
262 tool_name : string;
263 tool_input : Tool_input.t;
264 tool_result : Jsont.json;
265 context : context;
266 }
267
268 type decision =
269 | Continue
270 | Block of { reason : string option }
271 | AddContext of { context : string }
272
273 val continue : unit -> decision
274 val block : ?reason:string -> unit -> decision
275 val add_context : string -> decision
276
277 type handler = input -> decision
278end
279
280(** {1 Other Hooks} *)
281module UserPromptSubmit : sig
282 type input = { prompt : string; context : context }
283 type decision = Continue | Block of { reason : string option }
284 type handler = input -> decision
285end
286
287module Stop : sig
288 type input = { stop_hook_active : bool; context : context }
289 type decision = Continue | Block of { reason : string option }
290 type handler = input -> decision
291end
292
293module PreCompact : sig
294 type input = { context : context }
295 type handler = input -> unit (* Notification only *)
296end
297
298(** {1 Hook Configuration} *)
299module Config : sig
300 type t
301
302 val empty : t
303
304 val on_pre_tool_use : ?pattern:string -> PreToolUse.handler -> t -> t
305 val on_post_tool_use : ?pattern:string -> PostToolUse.handler -> t -> t
306 val on_user_prompt_submit : UserPromptSubmit.handler -> t -> t
307 val on_stop : Stop.handler -> t -> t
308 val on_pre_compact : PreCompact.handler -> t -> t
309end
310```
311
312### Options
313
314```ocaml
315(* options.mli *)
316
317(** Configuration options for Claude sessions.
318
319 {[
320 let options = Options.default
321 |> Options.with_model Model.opus
322 |> Options.with_mcp_server ~name:"tools" my_server
323 |> Options.with_allowed_tools ["mcp__tools__greet"; "Read"; "Write"]
324 |> Options.with_hooks my_hooks
325 |> Options.with_max_budget_usd 1.0
326 ]} *)
327
328type t
329
330val default : t
331
332(** {1 Builder Pattern} *)
333
334val with_system_prompt : string -> t -> t
335val with_append_system_prompt : string -> t -> t
336val with_model : Model.t -> t -> t
337val with_fallback_model : Model.t -> t -> t
338val with_max_turns : int -> t -> t
339val with_max_thinking_tokens : int -> t -> t
340val with_max_budget_usd : float -> t -> t
341
342val with_allowed_tools : string list -> t -> t
343val with_disallowed_tools : string list -> t -> t
344val with_permission_mode : Permission_mode.t -> t -> t
345
346val with_cwd : [> Eio.Fs.dir_ty ] Eio.Path.t -> t -> t
347val with_env : (string * string) list -> t -> t
348
349val with_mcp_server : name:string -> Mcp_server.t -> t -> t
350(** Add an in-process MCP server. Multiple servers can be added. *)
351
352val with_hooks : Hook.Config.t -> t -> t
353
354val with_no_settings : t -> t
355val with_cli_path : string -> t -> t
356
357(** {1 Accessors} *)
358
359val system_prompt : t -> string option
360val model : t -> Model.t option
361val mcp_servers : t -> (string * Mcp_server.t) list
362val hooks : t -> Hook.Config.t option
363(* ... other accessors ... *)
364```
365
366### Permission Mode
367
368```ocaml
369(* permission_mode.mli *)
370
371(** Permission modes for tool authorization. *)
372
373type t =
374 | Default (** Prompt for all permissions *)
375 | Accept_edits (** Auto-accept file edits *)
376 | Plan (** Planning mode - restricted execution *)
377 | Bypass (** Skip all permission checks - DANGEROUS *)
378
379val to_string : t -> string
380val of_string : string -> t option
381```
382
383### Model
384
385```ocaml
386(* model.mli *)
387
388(** Claude AI model identifiers. *)
389
390type t =
391 | Sonnet_4_5
392 | Opus_4
393 | Haiku_4
394 | Custom of string
395
396val sonnet : t
397val opus : t
398val haiku : t
399
400val to_string : t -> string
401val of_string : string -> t
402```
403
404### Messages and Responses
405
406```ocaml
407(* message.mli *)
408
409(** Messages exchanged with Claude. *)
410
411module Content_block : sig
412 type t =
413 | Text of { text : string }
414 | Tool_use of { id : string; name : string; input : Jsont.json }
415 | Tool_result of { tool_use_id : string; content : Jsont.json; is_error : bool }
416 | Thinking of { text : string }
417end
418
419module User : sig
420 type t
421 val of_string : string -> t
422 val of_blocks : Content_block.t list -> t
423 val of_tool_results : (string * Jsont.json * bool) list -> t
424end
425
426module Assistant : sig
427 type t
428 val content : t -> Content_block.t list
429 val text : t -> string (* Concatenated text blocks *)
430end
431
432type t =
433 | User of User.t
434 | Assistant of Assistant.t
435 | System of { session_id : string option }
436 | Result of { text : string }
437
438
439(* response.mli *)
440
441(** Response events from Claude. *)
442
443module Text : sig
444 type t
445 val content : t -> string
446end
447
448module Tool_use : sig
449 type t
450 val id : t -> string
451 val name : t -> string
452 val input : t -> Jsont.json
453end
454
455module Thinking : sig
456 type t
457 val content : t -> string
458end
459
460module Complete : sig
461 type t
462 val total_cost_usd : t -> float option
463 val input_tokens : t -> int
464 val output_tokens : t -> int
465 val duration_ms : t -> int option
466end
467
468module Init : sig
469 type t
470 val session_id : t -> string option
471end
472
473module Error : sig
474 type t
475 val message : t -> string
476 val code : t -> string option
477end
478
479type t =
480 | Text of Text.t
481 | Tool_use of Tool_use.t
482 | Thinking of Thinking.t
483 | Init of Init.t
484 | Error of Error.t
485 | Complete of Complete.t
486```
487
488---
489
490## Client Interface
491
492```ocaml
493(* client.mli *)
494
495(** Bidirectional client for Claude interactions.
496
497 The client handles:
498 - Message streaming via Eio
499 - MCP routing for custom tools
500 - Hook callbacks
501 - Permission requests
502 - Dynamic control (model/permission changes)
503
504 {2 Basic Usage}
505
506 {[
507 Eio.Switch.run @@ fun sw ->
508 let client = Client.create ~sw ~process_mgr ~clock () in
509
510 Client.query client "What is 2+2?";
511
512 Client.receive client |> Seq.iter (function
513 | Response.Text t -> print_endline (Response.Text.content t)
514 | Response.Complete c ->
515 Printf.printf "Cost: $%.4f\n"
516 (Option.value ~default:0.0 (Response.Complete.total_cost_usd c))
517 | _ -> ())
518 ]}
519
520 {2 With Custom Tools}
521
522 {[
523 let greet = Tool.create
524 ~name:"greet"
525 ~description:"Greet someone"
526 ~input_schema:(`O ["name", `String "string"])
527 ~handler:(fun args -> Ok (`String "Hello!"))
528
529 let server = Mcp_server.create ~name:"tools" ~tools:[greet] ()
530
531 let options = Options.default
532 |> Options.with_mcp_server ~name:"tools" server
533 |> Options.with_allowed_tools ["mcp__tools__greet"]
534
535 let client = Client.create ~sw ~process_mgr ~clock ~options ()
536 ]} *)
537
538type t
539
540val create :
541 sw:Eio.Switch.t ->
542 process_mgr:_ Eio.Process.mgr ->
543 clock:float Eio.Time.clock_ty Eio.Resource.t ->
544 ?options:Options.t ->
545 unit ->
546 t
547(** Create a new Claude client. *)
548
549(** {1 Querying} *)
550
551val query : t -> string -> unit
552(** [query t prompt] sends a text prompt to Claude. *)
553
554val send_message : t -> Message.User.t -> unit
555(** [send_message t msg] sends a user message (can include tool results). *)
556
557(** {1 Receiving Responses} *)
558
559val receive : t -> Response.t Seq.t
560(** [receive t] returns a lazy sequence of response events.
561
562 Built-in tool executions happen internally (by Claude CLI).
563 Custom tool calls are routed to MCP servers automatically.
564 You only see the responses. *)
565
566val receive_all : t -> Response.t list
567(** [receive_all t] collects all responses into a list. *)
568
569(** {1 Dynamic Control} *)
570
571val set_model : t -> Model.t -> unit
572val set_permission_mode : t -> Permission_mode.t -> unit
573val get_server_info : t -> Server_info.t
574val interrupt : t -> unit
575
576val session_id : t -> string option
577(** Get session ID if available. *)
578```
579
580---
581
582## Simple Query API
583
584```ocaml
585(* claude.mli *)
586
587(** OCaml SDK for Claude Code CLI.
588
589 {1 Quick Start}
590
591 {[
592 open Eio.Std
593
594 let () = Eio_main.run @@ fun env ->
595 Switch.run @@ fun sw ->
596 let process_mgr = Eio.Stdenv.process_mgr env in
597 let clock = Eio.Stdenv.clock env in
598
599 (* Simple one-shot query *)
600 let response = Claude.query_text ~sw ~process_mgr ~clock
601 ~prompt:"What is 2+2?" () in
602 print_endline response
603 ]}
604
605 {1 With Custom Tools}
606
607 {[
608 let greet = Claude.Tool.create
609 ~name:"greet"
610 ~description:"Greet a user"
611 ~input_schema:(`O ["name", `String "string"])
612 ~handler:(fun args ->
613 Ok (`String (Printf.sprintf "Hello, %s!"
614 (Jsont.get_string_exn "name" args))))
615
616 let server = Claude.Mcp_server.create
617 ~name:"my-tools"
618 ~tools:[greet]
619 ()
620
621 let options = Claude.Options.default
622 |> Claude.Options.with_mcp_server ~name:"tools" server
623 |> Claude.Options.with_allowed_tools ["mcp__tools__greet"]
624
625 let client = Claude.Client.create ~sw ~process_mgr ~clock ~options ()
626 ]} *)
627
628(** {1 Simple Query Functions} *)
629
630val query :
631 sw:Eio.Switch.t ->
632 process_mgr:_ Eio.Process.mgr ->
633 clock:float Eio.Time.clock_ty Eio.Resource.t ->
634 ?options:Options.t ->
635 prompt:string ->
636 unit ->
637 Response.t Seq.t
638(** [query ~sw ~process_mgr ~clock ?options ~prompt ()] performs a one-shot query.
639
640 Returns a lazy sequence of response events. The client is created and
641 cleaned up automatically. *)
642
643val query_text :
644 sw:Eio.Switch.t ->
645 process_mgr:_ Eio.Process.mgr ->
646 clock:float Eio.Time.clock_ty Eio.Resource.t ->
647 ?options:Options.t ->
648 prompt:string ->
649 unit ->
650 string
651(** [query_text ...] is like [query] but returns concatenated text response. *)
652
653(** {1 Core Modules} *)
654
655module Client = Client
656module Options = Options
657module Tool = Tool
658module Mcp_server = Mcp_server
659module Hook = Hook
660module Message = Message
661module Response = Response
662module Model = Model
663module Permission_mode = Permission_mode
664module Server_info = Server_info
665module Err = Err
666```
667
668---
669
670## Error Handling
671
672```ocaml
673(* err.mli *)
674
675(** Structured error types. *)
676
677type t =
678 | Cli_not_found of string
679 | Process_error of { exit_code : int; message : string }
680 | Protocol_error of { message : string; raw : string option }
681 | Timeout of { operation : string }
682 | Permission_denied of { tool : string; reason : string }
683 | Hook_error of { hook : string; error : string }
684 | Mcp_error of { server : string; method_ : string; error : string }
685
686exception E of t
687
688val to_string : t -> string
689val raise_cli_not_found : string -> 'a
690val raise_process_error : exit_code:int -> message:string -> 'a
691val raise_protocol_error : message:string -> ?raw:string -> unit -> 'a
692val raise_timeout : operation:string -> 'a
693```
694
695---
696
697## Internal Architecture
698
699### Process Management
700
701The internal process module spawns Claude CLI and manages bidirectional communication:
702
703```ocaml
704(* internal/process.ml *)
705
706type t = {
707 proc : Eio.Process.t;
708 stdin : Eio.Flow.sink;
709 stdout : Eio.Flow.source;
710 stderr : Eio.Flow.source;
711}
712
713val spawn :
714 sw:Eio.Switch.t ->
715 process_mgr:_ Eio.Process.mgr ->
716 ?cli_path:string ->
717 ?cwd:Eio.Fs.dir_ty Eio.Path.t ->
718 args:string list ->
719 unit ->
720 t
721
722val send_line : t -> string -> unit
723val read_line : t -> string option
724val close : t -> unit
725```
726
727### Protocol Handling
728
729The protocol module handles JSON wire format:
730
731```ocaml
732(* internal/protocol.ml *)
733
734type incoming =
735 | Message of Message.t
736 | Control_request of {
737 request_id : string;
738 request : control_request;
739 }
740 | Control_response of {
741 request_id : string;
742 response : control_response;
743 }
744
745and control_request =
746 | Permission_request of { tool_name : string; input : Jsont.json }
747 | Hook_callback of { callback_id : string; input : Jsont.json }
748 | Mcp_request of { server : string; message : Jsont.json }
749
750and control_response =
751 | Success of { response : Jsont.json option }
752 | Error of { message : string }
753
754type outgoing =
755 | User_message of Message.User.t
756 | Control_request of { request : Request.t }
757 | Control_response of { request_id : string; response : Response.t }
758
759val decode : string -> incoming
760val encode : outgoing -> string
761```
762
763### MCP Handler
764
765Routes MCP requests to appropriate in-process servers:
766
767```ocaml
768(* internal/mcp_handler.ml *)
769
770type t
771
772val create : servers:(string * Mcp_server.t) list -> t
773
774val handle_request :
775 t ->
776 server:string ->
777 message:Jsont.json ->
778 Jsont.json
779(** Handle MCP JSONRPC request and return response. *)
780```
781
782---
783
784## Message Flow Diagrams
785
786### Built-in Tool Execution (passthrough)
787
788```
789User SDK Client Claude CLI Claude
790 | | | |
791 |--query()------>| | |
792 | |--UserMsg------>| |
793 | | |--API call--->|
794 | | |<--tool_use---|
795 | | | (Read file) |
796 | | | |
797 | | | [CLI executes Read internally]
798 | | | |
799 | | |--tool_result>|
800 | | |<--text-------|
801 | |<--Response-----| |
802 |<--Response.Text| | |
803```
804
805### Custom MCP Tool Execution
806
807```
808User SDK Client Claude CLI Claude
809 | | | |
810 |--query()------>| | |
811 | |--UserMsg------>| |
812 | | |--API call--->|
813 | | |<--tool_use---|
814 | | | (mcp__x__y) |
815 | |<--mcp_request--| |
816 | | | |
817 | | [SDK routes to Mcp_server] |
818 | | | |
819 | |--mcp_response->| |
820 | | |--tool_result>|
821 | | |<--text-------|
822 | |<--Response-----| |
823 |<--Response.Text| | |
824```
825
826### Hook Interception
827
828```
829User SDK Client Claude CLI Claude
830 | | | |
831 |--query()------>| | |
832 | |--UserMsg------>| |
833 | | |--API call--->|
834 | | |<--tool_use---|
835 | | | (Bash) |
836 | |<--hook_callback| |
837 | | [PreToolUse] | |
838 | | | |
839 | | [SDK runs hook, returns Deny] |
840 | | | |
841 | |--hook_response>| (denied) |
842 | | |--error msg-->|
843 | | |<--text-------|
844 | |<--Response-----| |
845 |<--Response.Text| | |
846```
847
848---
849
850## Migration from Current SDK
851
852### Key Changes
853
8541. **Remove explicit tool execution**
855 - Current: SDK receives tool_use, executes tool, returns result
856 - New: Built-in tools handled by CLI; only MCP tools executed by SDK
857
8582. **Add MCP server support**
859 - New: `Tool.t`, `Mcp_server.t` for custom tool definition
860
8613. **Simplify hooks**
862 - Current: Hooks can have complex tool execution logic
863 - New: Hooks intercept only; execution is separate
864
8654. **Clean up Handler module**
866 - Current: Object-oriented handler class
867 - New: Functional response handling via `Seq.t`
868
869### Compatibility Notes
870
871- `Options.with_hooks` remains similar
872- `Client.query/receive` API stays the same
873- New: `Options.with_mcp_server` for custom tools
874- Removed: Direct tool execution callbacks
875
876---
877
878## Example: Complete Application
879
880```ocaml
881open Eio.Std
882
883(* Define custom tools *)
884let calculator_add = Claude.Tool.create
885 ~name:"add"
886 ~description:"Add two numbers"
887 ~input_schema:(`O [
888 "a", `O ["type", `String "number"];
889 "b", `O ["type", `String "number"];
890 ])
891 ~handler:(fun args ->
892 match Jsont.(find_float "a" args, find_float "b" args) with
893 | Some a, Some b -> Ok (`String (Printf.sprintf "%.2f" (a +. b)))
894 | _ -> Error "Missing a or b parameter")
895
896let calculator_multiply = Claude.Tool.create
897 ~name:"multiply"
898 ~description:"Multiply two numbers"
899 ~input_schema:(`O [
900 "a", `O ["type", `String "number"];
901 "b", `O ["type", `String "number"];
902 ])
903 ~handler:(fun args ->
904 match Jsont.(find_float "a" args, find_float "b" args) with
905 | Some a, Some b -> Ok (`String (Printf.sprintf "%.2f" (a *. b)))
906 | _ -> Error "Missing a or b parameter")
907
908(* Create MCP server *)
909let calculator_server = Claude.Mcp_server.create
910 ~name:"calculator"
911 ~version:"1.0.0"
912 ~tools:[calculator_add; calculator_multiply]
913 ()
914
915(* Define hook to block dangerous commands *)
916let block_dangerous_bash input =
917 if input.Claude.Hook.PreToolUse.tool_name = "Bash" then
918 match Claude.Tool_input.get_string input.tool_input "command" with
919 | Some cmd when String.is_substring cmd ~substring:"rm -rf" ->
920 Claude.Hook.PreToolUse.deny ~reason:"Dangerous command blocked"
921 | _ -> Claude.Hook.PreToolUse.allow ()
922 else Claude.Hook.PreToolUse.allow ()
923
924let hooks = Claude.Hook.Config.empty
925 |> Claude.Hook.Config.on_pre_tool_use ~pattern:"Bash" block_dangerous_bash
926
927(* Main application *)
928let () = Eio_main.run @@ fun env ->
929 Switch.run @@ fun sw ->
930 let process_mgr = Eio.Stdenv.process_mgr env in
931 let clock = Eio.Stdenv.clock env in
932
933 let options = Claude.Options.default
934 |> Claude.Options.with_model Claude.Model.opus
935 |> Claude.Options.with_mcp_server ~name:"calc" calculator_server
936 |> Claude.Options.with_allowed_tools [
937 "mcp__calc__add";
938 "mcp__calc__multiply";
939 "Read";
940 "Bash";
941 ]
942 |> Claude.Options.with_hooks hooks
943 |> Claude.Options.with_max_budget_usd 0.50
944 in
945
946 let client = Claude.Client.create ~sw ~process_mgr ~clock ~options () in
947
948 (* Multi-turn conversation *)
949 Claude.Client.query client "What is 23 + 45?";
950 Claude.Client.receive client |> Seq.iter (function
951 | Claude.Response.Text t ->
952 Printf.printf "Claude: %s\n" (Claude.Response.Text.content t)
953 | Claude.Response.Tool_use tu ->
954 Printf.printf "[Using tool: %s]\n" (Claude.Response.Tool_use.name tu)
955 | Claude.Response.Complete c ->
956 Printf.printf "[Cost: $%.4f]\n"
957 (Option.value ~default:0.0 (Claude.Response.Complete.total_cost_usd c))
958 | _ -> ());
959
960 Claude.Client.query client "Now multiply that result by 2";
961 Claude.Client.receive_all client |> ignore
962```
963
964---
965
966## Implementation Priority
967
9681. **Phase 1: Core Types**
969 - `Tool.t`, `Mcp_server.t`
970 - Updated `Options.t` with MCP support
971 - `Permission_mode.t`, `Model.t`
972
9732. **Phase 2: Internal MCP Routing**
974 - `internal/mcp_handler.ml`
975 - Protocol updates for MCP messages
976 - Remove built-in tool execution from client
977
9783. **Phase 3: Hook Simplification**
979 - Update `Hook` module to intercept-only model
980 - Remove tool execution from hook callbacks
981
9824. **Phase 4: API Polish**
983 - Simple `query` function
984 - Documentation and examples
985 - Error handling improvements
986
9875. **Phase 5: Testing & Migration**
988 - Comprehensive tests
989 - Migration guide
990 - Deprecation of old patterns