# YiemAgent Julia framework for building agents with tool use and MCP (Model Context Protocol) support. ## Getting Started 1. Install dependencies: `]add JSON, DataStructures, UUIDs, Dates, NATS, DataFrames` 2. Create a callable `mcpServer` struct that communicates with an MCP server via NATS 3. Create a `yiemAgent` with an LLM callable and MCP server: ```julia agent = YiemAgent.yiemAgent(llmCall; mcpServer=mcpServer, eventSink=yourSink) ``` 4. Call `runAgent(agent, message)` then `takeResponse(agent)` ## Architecture ``` src/ ├── YiemAgent.jl # Module entry point ├── type.jl # Core types (messages, tools, agent state) ├── utils.jl # Message formatting, context preparation, validation ├── agentCore.jl # Agent loop, tool execution pipeline ├── api.jl # Public API (runAgent, takeResponse, followUp, stopAgent) └── toolRegistry.jl # Tool store, MCP discovery, listTools registration ``` ## MCP Protocol YiemAgent uses JSON-RPC 2.0 for MCP communication. The `mcpServer` callable struct must implement: ```julia # tools/list — returns tool definitions mcpServer("tools/list") # → Dict("jsonrpc"=>"2.0", "id"=>1, "result"=>Dict("tools"=>[...], "nextCursor"=>...)) # tools/call — executes a tool mcpServer("tools/call", toolName, arguments) # → Dict("jsonrpc"=>"2.0", "id"=>2, "result"=>Dict("content"=>[...], "isError"=>...)) ``` Protocol error responses include `"error"` instead of `"result"`: ```julia Dict("jsonrpc"=>"2.0", "id"=>2, "error"=>Dict("code"=>-32602, "message"=>"...")) ``` ### Implementing the MCP Server Client You must provide a callable struct that communicates with your MCP server. Example using NATS: ```julia struct mcpServer natsConn::NATS.Connection topic::String senderID::String fileserver_url::String end # tools/list implementation function (c::mcpServer)(method::String) if method != "tools/list" error("mcpServer: unexpected method '$method' (expected 'tools/list')") end payload = Dict("jsonrpc" => "2.0", "id" => 1, "method" => method, "params" => Dict{String, Any}()) payloads = [("payload", payload, "dictionary")] _, msg_envelope_json_str = msghandler.smartpack( c.topic, payloads; sender_id=c.senderID, msg_purpose="mcp_tools_list", fileserver_url=c.fileserver_url) reply = NATS.request(c.natsConn, c.topic, msg_envelope_json_str, timeout=180) incoming_env = msghandler.smartunpack(String(reply.payload)) return incoming_env["payloads"][1][2] end # tools/call implementation function (c::mcpServer)(method::String, toolName::String, arguments::Dict{String, Any}) if method != "tools/call" error("mcpServer: unexpected method '$method' (expected 'tools/call')") end payload = Dict( "jsonrpc" => "2.0", "id" => 2, "method" => method, "params" => Dict( "name" => toolName, "arguments" => arguments ) ) payloads = [("payload", payload, "dictionary")] _, msg_envelope_json_str = msghandler.smartpack( c.topic, payloads; sender_id=c.senderID, msg_purpose="mcp_tool_call", fileserver_url=c.fileserver_url) reply = NATS.request(c.natsConn, c.topic, msg_envelope_json_str, timeout=180) incoming_env = msghandler.smartunpack(String(reply.payload)) return incoming_env["payloads"][1][2] end ``` See `test/runtest.jl` for the complete working example. ## Tool Discovery Tools are discovered dynamically via MCP server: 1. `listTool` is the only pre-registered tool 2. When the LLM calls `listTools()`, tools are discovered from the MCP server and registered at runtime 3. Supports pagination via `nextCursor` for large tool sets ## Agent API | Function | Description | |----------|-------------| | `runAgent(agent, msg)` | Send a message to the agent's input channel | | `takeResponse(agent)` | Block and take the agent's response from output channel | | `followUp(agent, msg)` | Send a follow-up message while agent is still processing | | `stopAgent(agent)` | Gracefully stop the agent and close channels | ## Agent Lifecycle 1. `yiemAgent()` spawns a background task (`_agentLoop`) listening on `inputChannel` and `followUpChannel` 2. User messages enter via `runAgent()` or `followUp()` 3. `_processMessage()` handles LLM calls, tool execution, and conversation history 4. Tool execution follows three phases: `prepareToolCall` → `executePreparedToolCall` → `finalizeExecutedToolCall` 5. `beforeToolCall`/`afterToolCall` hooks allow pre/post-processing of tool calls 6. Tool `execute` functions can set `terminate=true` to stop the agent loop ## Hooks | Hook | Signature | Purpose | |------|-----------|---------| | `prepareContext` | `(state, sink, llmCall) -> ctx` | Transform messages/context before LLM call | | `formatMsgForLLM` | `(ctx, sink) -> dict` | Convert agent context to LLM API format | | `beforeToolCall` | `(context, signal) -> result` | Block/allow tool execution | | `afterToolCall` | `(context, signal) -> result` | Post-process tool results | | `eventSink` | `(msg) -> nothing` | Callback for agent events/debug messages |