MCP Client & Server

Types and functions for the Model Context Protocol — connecting to MCP servers and building MCP servers in Julia.

Client Types

UniLM.MCPSessionType
MCPSession

A 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!.

source
UniLM.MCPToolResultType
MCPToolResult

The 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 — text parts joined with \n, each non-text part JSON-encoded.
  • structured::Union{Nothing,Dict{String,Any}}: the server's structuredContent object verbatim when present, otherwise nothing.
  • is_error::Bool: true when the server flagged the call as a tool-execution error (isError); the detail is carried in content.
  • parts::Vector{Any}: the raw content array exactly as received, before it is rendered into content::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.

source
UniLM.MCPTransportType
MCPTransport

Abstract 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
source
UniLM.StdioTransportType
StdioTransport <: MCPTransport

Stdio transport: launches a subprocess and communicates via stdin/stdout. Messages are newline-delimited JSON-RPC 2.0.

source
UniLM.HTTPTransportType
HTTPTransport <: MCPTransport

Streamable HTTP transport: communicates via POST requests to an MCP endpoint. Handles Mcp-Session-Id header for session management.

source
UniLM.MCPErrorType
MCPError <: Exception

Error from MCP protocol operations. Contains the JSON-RPC error code, message, and optional data from the server.

source
UniLM.MCPCrashErrorType
MCPCrashError <: Exception

The 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; else nothing.
  • termsignal::Union{Int,Nothing}: signal number when the server was signal-killed; else nothing.
  • cause::Union{Exception,Nothing}: underlying transport exception (nothing for 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).

source

Client Functions

Lifecycle

UniLM.mcp_connectFunction
mcp_connect(command::Cmd; client_name="UniLM.jl", protocol_version="2025-11-25",
            config=nothing, auto_respawn=false) -> MCPSession

Connect 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)
source
mcp_connect(url::String; headers=[], client_name="UniLM.jl", protocol_version="2025-11-25",
            config=nothing, auto_respawn=false) -> MCPSession

Connect 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"])
source
mcp_connect(transport::MCPTransport; client_name="UniLM.jl", protocol_version="2025-11-25",
            config=nothing, auto_respawn=false) -> MCPSession

Connect 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.

source
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)
end
source
UniLM.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.

source

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).

source
UniLM.list_resources!Function
list_resources!(session::MCPSession) -> Vector{MCPResourceInfo}

Fetch the resource list from the MCP server. Handles pagination.

source
UniLM.list_prompts!Function
list_prompts!(session::MCPSession) -> Vector{MCPPromptInfo}

Fetch the prompt list from the MCP server. Handles pagination.

source

Operations

UniLM.call_toolFunction
call_tool(session::MCPSession, name::String, arguments::AbstractDict) -> MCPToolResult

Call 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).

source
UniLM.read_resourceFunction
read_resource(session::MCPSession, uri::String) -> String

Read a resource from the MCP server.

source
UniLM.get_promptFunction
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.

source
UniLM.pingFunction
ping(session::MCPSession)

Send a ping to the MCP server. Throws on error.

source

Tool Bridge

UniLM.mcp_toolsFunction
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)
source
UniLM.mcp_tools_respondFunction
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)
source

Server Types

UniLM.MCPServerType
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 default
source
UniLM.MCPServerToolType
MCPServerTool <: MCPServerPrimitive

A 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 name
  • description::Union{String,Nothing}: Human-readable description
  • input_schema::Dict{String,Any}: JSON Schema for the input parameters
  • handler::Function: (args::Dict{String,Any}) -> Any
source
UniLM.MCPServerResourceType
MCPServerResource <: MCPServerPrimitive

A static resource registered on an MCP server.

Fields

  • uri::String: Resource URI
  • name::String: Human-readable name
  • description::Union{String,Nothing}: Description
  • mime_type::String: MIME type (default "text/plain")
  • handler::Function: () -> Union{String, Vector{UInt8}}
source
UniLM.MCPServerResourceTemplateType
MCPServerResourceTemplate <: MCPServerPrimitive

A 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 name
  • description::Union{String,Nothing}: Description
  • mime_type::String: MIME type
  • handler::Function: (params::Dict{String,String}) -> Union{String, Vector{UInt8}}
  • _pattern::Regex: Compiled regex from template
  • _param_names::Vector{String}: Extracted parameter names
source
UniLM.MCPServerPromptType
MCPServerPrompt <: MCPServerPrimitive

A prompt template registered on an MCP server.

Fields

  • name::String: Unique prompt name
  • description::Union{String,Nothing}: Description
  • arguments::Vector{Dict{String,Any}}: Argument definitions
  • handler::Function: (args::Dict{String,Any}) -> Vector{Dict{String,Any}}
source

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.

source
register_tool!(server, name, description, handler)

Register a tool with schema auto-inferred from the handler's type signature.

source
register_tool!(server, ct::CallableTool{Tool})

Register a CallableTool{Tool} on the MCP server, bridging from UniLM's Chat Completions tool type.

source
register_tool!(server, ct::CallableTool{FunctionTool})

Register a CallableTool{FunctionTool} on the MCP server, bridging from UniLM's Responses API tool type.

source
UniLM.register_resource!Function
register_resource!(server, uri, name, handler; mime_type="text/plain", description=nothing)

Register a static resource on the MCP server.

source
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.

source
UniLM.register_prompt!Function
register_prompt!(server, name, handler; description=nothing, arguments=Dict{String,Any}[])

Register a prompt template on the MCP server.

source

Serving

UniLM.serveFunction
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. Accepts input/output IO overrides; returns after EOF on input.
  • :http: HTTP server. Accepts host (default "127.0.0.1"), port (default 8080), allowed_origins (extra allowed Origin header values — localhost origins and requests without an Origin are always accepted; anything else gets 403), and block (default true: block until the server is closed; block=false returns the running server — close it with close).

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)
source

Macros

UniLM.@mcp_toolMacro
@mcp_tool server function name(args...)::ReturnType body end

Register 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)
end
source
UniLM.@mcp_resourceMacro
@mcp_resource server uri_or_template function(args...) body end

Register 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)
end
source
UniLM.@mcp_promptMacro
@mcp_prompt server name function(args...) body end

Register 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"))]
end
source

Example

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"]