MCP Client & Server
Types and functions for the Model Context Protocol — connecting to MCP servers and building MCP servers in Julia.
Client Types
UniLM.MCPSession — Type
MCPSessionA live connection to an MCP server. Manages lifecycle, transport, and cached tool/resource/prompt lists.
Create via mcp_connect. Disconnect via mcp_disconnect!.
Requests are serialized: each call (its liveness/respawn check, id allocation and request/response exchange) runs under an internal session lock, and interleaved server → client frames are handled in place (notifications skipped, server ping requests answered). A concurrent caller therefore waits for the call in progress, and mcp_request_timeout bounds its own exchange — measured from the moment it takes the lock, not from the moment it asked — so one call's wait behind another never counts against it, and never tears down a healthy exchange in progress. The wait itself stays bounded, transitively: the call ahead runs under that same per-exchange bound. After the server sends notifications/tools/list_changed, tools_stale is true until the next list_tools!.
UniLM.MCPToolInfo — Type
MCPToolInfoA tool definition received from an MCP server via tools/list. Convert to CallableTool via mcp_tools or mcp_tools_respond.
UniLM.MCPToolResult — Type
MCPToolResultThe typed result of a call_tool call, mirroring an MCP tools/call result. A tool-execution error (isError: true on the wire) is reported here as data (is_error == true), not thrown, so callers can distinguish it from a JSON-RPC protocol error (which still throws MCPError).
Fields
content::String: the content parts rendered to text —textparts joined with\n, each non-text part JSON-encoded.structured::Union{Nothing,Dict{String,Any}}: the server'sstructuredContentobject verbatim when present, otherwisenothing.is_error::Bool:truewhen the server flagged the call as a tool-execution error (isError); the detail is carried incontent.parts::Vector{Any}: the rawcontentarray exactly as received, before it is rendered intocontent::String.
The mcp_tools / mcp_tools_respond bridges surface this to a tool-calling loop: content on success (falling back to a JSON encoding of structured when content is empty), or a raised error carrying content when is_error.
UniLM.MCPResourceInfo — Type
MCPResourceInfoA resource definition received from an MCP server via resources/list.
UniLM.MCPPromptInfo — Type
MCPPromptInfoA prompt definition received from an MCP server via prompts/list.
UniLM.MCPServerCapabilities — Type
MCPServerCapabilitiesCapabilities declared by an MCP server during initialization.
UniLM.MCPTransport — Type
MCPTransportAbstract type for MCP transport implementations. Subtypes must implement:
_transport_connect!(t)— establish connection_transport_send!(t, msg::String)::String— send JSON-RPC message, return response_transport_read!(t)::String— read the next incoming JSON-RPC frame_transport_notify!(t, msg::String)— send notification (no response expected)_transport_disconnect!(t)— close connection_transport_isconnected(t)::Bool— check if connected
UniLM.StdioTransport — Type
StdioTransport <: MCPTransportStdio transport: launches a subprocess and communicates via stdin/stdout. Messages are newline-delimited JSON-RPC 2.0.
UniLM.HTTPTransport — Type
HTTPTransport <: MCPTransportStreamable HTTP transport: communicates via POST requests to an MCP endpoint. Handles Mcp-Session-Id header for session management.
UniLM.MCPError — Type
MCPError <: ExceptionError from MCP protocol operations. Contains the JSON-RPC error code, message, and optional data from the server.
UniLM.MCPCrashError — Type
MCPCrashError <: ExceptionThe stdio MCP server process exited or its stdio pipe broke. The session is closed with cause :crash; with auto_respawn=true the next call respawns the server, otherwise it errors naming the opt-in.
Fields
msg::String: human-readable message with recovery guidance.exitcode::Union{Int,Nothing}: exit code when the server exited on its own and had been reaped in time; elsenothing.termsignal::Union{Int,Nothing}: signal number when the server was signal-killed; elsenothing.cause::Union{Exception,Nothing}: underlying transport exception (nothingfor a clean read-EOF).
Best-effort diagnostics: both exitcode and termsignal are nothing when the process had not been reaped within a brief settle window after the failure surfaced (or was still alive with a broken pipe).
Client Functions
Lifecycle
UniLM.mcp_connect — Function
mcp_connect(command::Cmd; client_name="UniLM.jl", protocol_version="2025-11-25",
config=nothing, auto_respawn=false) -> MCPSessionConnect to an MCP server via stdio transport (subprocess).
config::Union{Nothing,RequestConfig} is resolved (config if given, else the ambient/process default) and captured on the session: config.mcp_connect_timeout bounds the spawn→initialize handshake, config.mcp_request_timeout is the default per-exchange bound. A stdio request timeout is session-fatal (no id demux), as is a server crash; with auto_respawn=true the next call respawns the server (same command, fresh handshake — in-memory server state is lost), otherwise it errors.
Example
session = mcp_connect(`npx -y @modelcontextprotocol/server-filesystem /tmp`)
tools = mcp_tools(session)
# ... use tools with tool_loop! ...
mcp_disconnect!(session)mcp_connect(url::String; headers=[], client_name="UniLM.jl", protocol_version="2025-11-25",
config=nothing, auto_respawn=false) -> MCPSessionConnect to an MCP server via HTTP transport.
config::Union{Nothing,RequestConfig} is resolved (config if given, else the ambient/process default) and captured on the session: config.mcp_connect_timeout bounds the connect step, config.mcp_request_timeout is the default per-exchange bound.
Example
session = mcp_connect("https://mcp.example.com/mcp";
headers=["Authorization" => "Bearer token"])mcp_connect(transport::MCPTransport; client_name="UniLM.jl", protocol_version="2025-11-25",
config=nothing, auto_respawn=false) -> MCPSessionConnect to an MCP server via the given transport. Performs initialization handshake and populates tool cache. config (a RequestConfig, default: the ambient configuration) is resolved and captured on the session — its mcp_connect_timeout bounds this handshake and mcp_request_timeout bounds each later exchange. auto_respawn=true lets a stdio session closed by a request timeout or a server crash respawn its server (same command, captured config) and retry the next call once.
mcp_connect(f::Function, args...; kwargs...)Do-block form: automatically disconnects after the block executes.
Example
mcp_connect(`npx server`) do session
tools = mcp_tools(session)
chat = Chat(tools=map(t -> t.tool, tools))
push!(chat, Message(Val(:user), "List files"))
tool_loop!(chat; tools)
endUniLM.mcp_disconnect! — Function
mcp_disconnect!(session::MCPSession)Gracefully disconnect from the MCP server.
Takes the session lock, so a disconnect racing a call in flight WAITS for that exchange to finish instead of tearing the transport down under its reader — the same concurrency-1 semantics every other call obeys. The wait stays bounded transitively: the exchange ahead runs under its own mcp_request_timeout.
Discovery
UniLM.list_tools! — Function
list_tools!(session::MCPSession) -> Vector{MCPToolInfo}Fetch the tool list from the MCP server. Handles pagination via cursor. Stores result in session.tools.
timeout::Union{Nothing,Float64} overrides the per-exchange bound for this call (kwarg > ambient with_request_config > session-captured config; Inf disables, NaN/≤0 rejected).
UniLM.list_resources! — Function
list_resources!(session::MCPSession) -> Vector{MCPResourceInfo}Fetch the resource list from the MCP server. Handles pagination.
UniLM.list_prompts! — Function
list_prompts!(session::MCPSession) -> Vector{MCPPromptInfo}Fetch the prompt list from the MCP server. Handles pagination.
Operations
UniLM.call_tool — Function
call_tool(session::MCPSession, name::String, arguments::AbstractDict) -> MCPToolResultCall a tool on the MCP server and return its result as an MCPToolResult.
content concatenates text content parts (non-text parts JSON-encoded); structured carries the server's structuredContent verbatim; parts is the raw content array. A tool-execution error (isError: true) is returned with is_error == true — it is not thrown. JSON-RPC protocol errors still throw MCPError.
timeout::Union{Nothing,Float64} overrides the per-exchange bound for this call (kwarg > ambient with_request_config > session-captured config; Inf disables, NaN/≤0 rejected).
UniLM.read_resource — Function
read_resource(session::MCPSession, uri::String) -> StringRead a resource from the MCP server.
UniLM.get_prompt — Function
get_prompt(session::MCPSession, name::String, arguments::AbstractDict=Dict()) -> Vector{Dict{String,Any}}Get a rendered prompt from the MCP server. Returns the messages array.
UniLM.ping — Function
ping(session::MCPSession)Send a ping to the MCP server. Throws on error.
Tool Bridge
UniLM.mcp_tools — Function
mcp_tools(session::MCPSession) -> Vector{CallableTool{Tool}}Convert all tools from an MCP session into CallableTool{Tool} instances that work directly with tool_loop! (Chat Completions API).
Each tool's callable invokes call_tool(session, name, args) under the hood.
Example
session = mcp_connect(`npx server`)
tools = mcp_tools(session)
chat = Chat(model="gpt-5.5", tools=map(t -> t.tool, tools))
push!(chat, Message(Val(:user), "Do something"))
result = tool_loop!(chat; tools)UniLM.mcp_tools_respond — Function
mcp_tools_respond(session::MCPSession) -> Vector{CallableTool{FunctionTool}}Convert all tools from an MCP session into CallableTool{FunctionTool} instances that work directly with tool_loop (Responses API).
Example
session = mcp_connect("https://mcp.example.com/mcp")
tools = mcp_tools_respond(session)
result = tool_loop("Do something"; tools=tools)Server Types
UniLM.MCPServer — Type
MCPServer(name, version; description=nothing)An MCP server that can host tools, resources, and prompts.
Register primitives via register_tool!, register_resource!, register_prompt!, or the @mcp_tool, @mcp_resource, @mcp_prompt macros.
Start serving via serve.
Example
server = MCPServer("my-server", "1.0.0")
register_tool!(server, "add", "Add two numbers",
Dict("type"=>"object", "properties"=>Dict("a"=>Dict("type"=>"number"),"b"=>Dict("type"=>"number")), "required"=>["a","b"]),
args -> string(args["a"] + args["b"]))
serve(server) # stdio by defaultUniLM.MCPServerPrimitive — Type
MCPServerPrimitiveAbstract supertype for MCP server-side primitives (tools, resources, prompts).
UniLM.MCPServerTool — Type
MCPServerTool <: MCPServerPrimitiveA tool registered on an MCP server. The handler receives Dict{String,Any} arguments and returns any value (converted to text content by the server).
Fields
name::String: Unique tool namedescription::Union{String,Nothing}: Human-readable descriptioninput_schema::Dict{String,Any}: JSON Schema for the input parametershandler::Function:(args::Dict{String,Any}) -> Any
UniLM.MCPServerResource — Type
MCPServerResource <: MCPServerPrimitiveA static resource registered on an MCP server.
Fields
uri::String: Resource URIname::String: Human-readable namedescription::Union{String,Nothing}: Descriptionmime_type::String: MIME type (default"text/plain")handler::Function:() -> Union{String, Vector{UInt8}}
UniLM.MCPServerResourceTemplate — Type
MCPServerResourceTemplate <: MCPServerPrimitiveA URI-templated resource. Template variables like {path} are extracted and passed to the handler.
Fields
uri_template::String: URI template (e.g.,"file://{path}")name::String: Human-readable namedescription::Union{String,Nothing}: Descriptionmime_type::String: MIME typehandler::Function:(params::Dict{String,String}) -> Union{String, Vector{UInt8}}_pattern::Regex: Compiled regex from template_param_names::Vector{String}: Extracted parameter names
UniLM.MCPServerPrompt — Type
MCPServerPrompt <: MCPServerPrimitiveA prompt template registered on an MCP server.
Fields
name::String: Unique prompt namedescription::Union{String,Nothing}: Descriptionarguments::Vector{Dict{String,Any}}: Argument definitionshandler::Function:(args::Dict{String,Any}) -> Vector{Dict{String,Any}}
Server Functions
Registration
UniLM.register_tool! — Function
register_tool!(server, name, description, input_schema, handler)Register a tool on the MCP server with an explicit JSON Schema.
handler(args::Dict{String,Any}) receives the client's arguments object. An exception it raises is NOT a protocol error: the message is relayed to the client as tool content with isError: true, so a model can see the failure and correct itself. Write handlers with that in mind — raise with a message you are willing to show both the model and the client, and never one carrying secrets or internals. Errors below the handler (in dispatch itself) answer a generic JSON-RPC -32603 instead, with the detail going to the server's logs.
register_tool!(server, name, description, handler)Register a tool with schema auto-inferred from the handler's type signature.
register_tool!(server, ct::CallableTool{Tool})Register a CallableTool{Tool} on the MCP server, bridging from UniLM's Chat Completions tool type.
register_tool!(server, ct::CallableTool{FunctionTool})Register a CallableTool{FunctionTool} on the MCP server, bridging from UniLM's Responses API tool type.
UniLM.register_resource! — Function
register_resource!(server, uri, name, handler; mime_type="text/plain", description=nothing)Register a static resource on the MCP server.
UniLM.register_resource_template! — Function
register_resource_template!(server, uri_template, name, handler; mime_type="text/plain", description=nothing)Register a URI-templated resource on the MCP server.
UniLM.register_prompt! — Function
register_prompt!(server, name, handler; description=nothing, arguments=Dict{String,Any}[])Register a prompt template on the MCP server.
Serving
UniLM.serve — Function
serve(server::MCPServer; transport=:stdio, kwargs...)Start the MCP server using the specified transport.
Transports
:stdio(default): Read from stdin, write to stdout. For Claude Desktop/CLI integration. Acceptsinput/outputIO overrides; returns after EOF oninput.:http: HTTP server. Acceptshost(default"127.0.0.1"),port(default8080),allowed_origins(extra allowedOriginheader values — localhost origins and requests without anOriginare always accepted; anything else gets 403), andblock(defaulttrue: block until the server is closed;block=falsereturns the running server — close it withclose).
Robustness contract
Both transports cap an incoming frame (stdio) or request body (HTTP) at 16 MiB and answer an oversized one with JSON-RPC -32600 rather than parsing it — parsing an attacker-sized payload allocates a multiple of it, which is an out-of-memory kill rather than a protocol error. Any unhandled error while dispatching a request is answered with a generic -32603 "Internal error" and logged locally, so one bad frame cannot take the transport down and no exception text (file paths, argument values) reaches the peer. Tool-handler exceptions are excluded: they reach the client as tool results — see register_tool!.
Examples
serve(server) # stdio (default)
serve(server; transport=:http, port=3000) # HTTP; blocks until closed
h = serve(server; transport=:http, block=false) # HTTP; returns the server
close(h)Macros
UniLM.@mcp_tool — Macro
@mcp_tool server function name(args...)::ReturnType body endRegister a tool on server with auto-generated JSON Schema from the function signature.
Example
server = MCPServer("calc", "1.0.0")
@mcp_tool server function add(a::Float64, b::Float64)::String
string(a + b)
endUniLM.@mcp_resource — Macro
@mcp_resource server uri_or_template function(args...) body endRegister a resource or resource template on server. If the URI contains {...} placeholders, it is registered as a template.
Examples
@mcp_resource server "config://app" function()
read("config.toml", String)
end
@mcp_resource server "file://{path}" function(path::String)
read(path, String)
endUniLM.@mcp_prompt — Macro
@mcp_prompt server name function(args...) body endRegister a prompt on server. The handler should return a Vector of message Dicts.
Example
@mcp_prompt server "review" function(code::String)
[Dict("role" => "user", "content" => Dict("type" => "text", "text" => "Review: $code"))]
endExample
using UniLM
using JSON
# Construct client info types
info = MCPToolInfo(Dict{String,Any}(
"name" => "read_file",
"description" => "Read a file from disk",
"inputSchema" => Dict{String,Any}(
"type" => "object",
"properties" => Dict{String,Any}(
"path" => Dict{String,Any}("type" => "string")
)
)
))
println("Tool: ", info.name, " — ", info.description)Tool: read_file — Read a file from disk# Build and populate a server
server = MCPServer("demo", "1.0.0")
register_tool!(server, "add", "Add two numbers",
Dict{String,Any}(
"type" => "object",
"properties" => Dict{String,Any}(
"a" => Dict{String,Any}("type" => "number"),
"b" => Dict{String,Any}("type" => "number")
),
"required" => ["a", "b"]
),
args -> string(args["a"] + args["b"]))
println("Server: ", server.name, " v", server.version)
println("Tools: ", collect(keys(server.tools)))Server: demo v1.0.0
Tools: ["add"]