OCaml Claude SDK using Eio and Jsont
at main 990 lines 26 kB view raw view rendered
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