IonClaw supports the Model Context Protocol (MCP) both as a server and as a client.
- MCP Server — exposes IonClaw agents to external AI clients (Claude Code, Cursor, GitHub Copilot, etc.)
- MCP Client — lets IonClaw agents connect to external MCP servers and use their tools and resources (via the built-in
mcp_clienttool)
IonClaw implements MCP using the Streamable HTTP transport. Supported protocol versions: 2024-11-05, 2025-03-26, 2025-11-25. This lets any MCP-compatible AI client connect directly to IonClaw to chat with agents and manage sessions.
All MCP traffic goes through a single endpoint on the IonClaw HTTP server:
POST http://<host>:<port>/mcp — JSON-RPC request / response
GET http://<host>:<port>/mcp — SSE keepalive stream
DELETE http://<host>:<port>/mcp — close MCP session
The port is whatever server.port is set to in config.yml (default: 8080).
Authentication is optional. When enabled, every request must include a static Bearer token:
Authorization: Bearer <token>
- Settings → Credentials → Add a credential of type
Simple. Use the generate button (↺) on the Key field to fill it with a UUID. - Settings → Channels → MCP → Enable Require Token Authentication, select the credential, Save.
- Configure the client with
Authorization: Bearer <the-key-value>.
The token is verified by direct comparison against the credential's key — no login or JWT flow required.
channels:
mcp:
enabled: false
require_auth: true
credential: mcp_token # credential name whose key = the Bearer tokenThe server validates the Origin header on all requests per MCP spec 2025-11-25 to prevent DNS rebinding attacks:
- With auth enabled: all origins are allowed (the Bearer token provides access control).
- Without auth: only local origins (
localhost,127.0.0.1,[::1]) are allowed. Non-local origins receive403 Forbidden.
- Client sends
POST /mcpwith methodinitialize(noMCP-Session-Idheader). - Server creates a session and responds
200 OKwithMCP-Session-Id: <uuid>header plus capabilities. - Client sends
POST /mcpwith methodnotifications/initializedand the session ID header → server responds202 Accepted. - All subsequent requests must include
MCP-Session-Id: <uuid>. - If the session is not found, server responds
404 Not Found— client must start a new session with a freshinitialize. - Client sends
DELETE /mcpto close the session cleanly (optional).
Requests and responses follow JSON-RPC 2.0.
| Method | Description |
|---|---|
initialize |
Handshake — negotiates protocol version and returns capabilities |
notifications/initialized |
Client confirms initialization |
ping |
Returns an empty result |
tools/list |
Lists available tools |
tools/call |
Executes a tool |
resources/list |
Lists available static resources |
resources/templates/list |
Lists URI templates |
resources/read |
Reads a resource by URI |
| Code | Name | Meaning |
|---|---|---|
-32700 |
Parse error | Request body is not valid JSON |
-32600 |
Invalid request | Malformed JSON-RPC or missing session |
-32601 |
Method not found | Unknown method or tool name |
-32602 |
Invalid params | Required parameter is missing |
-32603 |
Internal error | Unexpected server error |
For tools/call, include Accept: text/event-stream to receive a streaming SSE response. The spec recommends including both application/json and text/event-stream in the Accept header.
The server sends notifications/progress events for each text chunk, followed by the final JSON-RPC result:
event: message
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"<token>","message":"<chunk>"}}
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"<full response>"}],"isError":false}}
Without Accept: text/event-stream, the server blocks until the full response is ready and returns it as application/json.
The progressToken is taken from _meta.progressToken in the request params if provided, otherwise defaults to the internal task ID. The original type is preserved: if the client sends a number (e.g. Cursor), the server echoes it back as a number.
Both streaming and non-streaming modes have a 300-second idle timeout. If the agent does not respond within this window, the server returns an error.
Send a message to the AI agent and receive a response.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
message |
string | yes | The message to send |
session_id |
string | no | Chat session ID. Uses the MCP session's own chat if omitted. May be a full key (mcp:uuid) or bare UUID |
agent |
string | no | Target agent name. If omitted, the classifier routes to the best agent |
Returns — plain text: the agent's full response.
Streaming — supports Accept: text/event-stream; tokens arrive as notifications/progress events.
Abort an active agent session mid-turn.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
session_id |
string | yes | Session key to abort (e.g. mcp:uuid or bare UUID) |
Returns — {"status": "aborted", "session_id": "..."} or {"status": "no_active_turn", ...}.
List all chat sessions across all channels.
Parameters — none.
Returns
{
"sessions": [
{
"key": "mcp:abc123",
"channel": "mcp",
"display_name": "Hello, how are...",
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:01:00Z"
}
],
"count": 1
}Get session details and full message history.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
session_id |
string | yes | Full session key (e.g. mcp:uuid or web:uuid) |
Returns
{
"key": "mcp:abc123",
"messages": [
{"role": "user", "content": "Hello", "timestamp": "..."},
{"role": "assistant", "content": "Hi!", "timestamp": "..."}
],
"created_at": "...",
"updated_at": "..."
}Delete a chat session and its persisted history.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
session_id |
string | yes | Full session key to delete |
Returns — {"status": "deleted", "session_id": "..."}.
List all configured agents.
Parameters — none.
Returns
{
"agents": [
{"name": "main", "description": "General-purpose assistant", "model": "anthropic/claude-sonnet-4-20250514"}
]
}List recent tasks.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit |
integer | no | Maximum tasks to return (default: 50) |
Returns
{
"tasks": [
{
"id": "task_abc",
"title": "Hello",
"state": "done",
"channel": "mcp",
"created_at": "...",
"result": "Hi!"
}
],
"count": 1
}Get full task details by ID.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
task_id |
string | yes | Task ID |
Returns — full task object (same fields as list_tasks entries, plus usage, iteration_count, etc.).
Resources expose IonClaw state as read-only URI-addressed data.
List of all sessions. Returns the same structure as list_sessions.
Session messages for a specific chat ID (bare UUID, without channel prefix).
List of all configured agents. Same structure as list_agents.
channels:
mcp:
enabled: false # auto-start on server boot (default: false)
require_auth: false # require Bearer token on all requests (default: false)
credential: "" # credential name whose key = the Bearer tokenenabled is a persistent config flag — it controls whether the MCP channel starts automatically when the server boots. It is saved to config.yml and only changes when you explicitly save the channel config.
The running state is transient — it reflects whether the channel is currently active. Starting or stopping the channel does not affect enabled.
- To configure auto-start: toggle
enabledand click Save in the web app. - To start/stop the channel at runtime without affecting the config: use the Start/Stop button or the API.
When the MCP channel is stopped, all in-memory MCP sessions are cleared. Clients must re-initialize after a restart.
PUT /api/channels/mcp — update config (persists to config.yml)
POST /api/channels/mcp/start — start channel (runtime only)
POST /api/channels/mcp/stop — stop channel (runtime only)
Add to .mcp.json in your project root or ~/.claude.json for user-level config:
{
"mcpServers": {
"ionclaw": {
"type": "http",
"url": "http://localhost:8080/mcp",
"headers": {
"Authorization": "Bearer <your-token>"
}
}
}
}In VS Code settings.json:
{
"github.copilot.chat.mcpServers": {
"ionclaw": {
"type": "http",
"url": "http://localhost:8080/mcp",
"headers": {
"Authorization": "Bearer <your-token>"
}
}
}
}In Cursor's MCP settings (~/.cursor/mcp.json):
{
"mcpServers": {
"ionclaw": {
"type": "http",
"url": "http://localhost:8080/mcp",
"headers": {
"Authorization": "Bearer <your-token>"
}
}
}
}If require_auth is false, omit the headers block.
Each MCP connection gets its own chat session. The session key follows the agent-scoped pattern agent:{agentId}:mcp:{uuid} where {uuid} is the MCP-Session-Id value. The API exposes sessions using the base key format (mcp:{uuid}). Sessions appear in the web app alongside web and Telegram sessions and share the same memory and agent context.
Sessions automatically get a display name derived from the first user message (truncated to ~50 characters at a word boundary). This makes MCP sessions easy to identify in the web app sidebar — e.g., "Hello, can you help me with..." instead of a raw UUID. If no user message has been sent yet, the sidebar shows a short ID like "MCP f4ec566c".
When a provider is unreachable or misconfigured, IonClaw returns clear, actionable error messages instead of raw exception text:
| Error Type | Example Message |
|---|---|
| Host not found | Could not connect to provider 'llama': the host was not found. Please check that the provider's base_url is correct and the service is reachable. (model: llama/local) |
| Authentication | Authentication failed for model 'anthropic/claude-sonnet-4-20250514'. Please check that the API key is valid and has the required permissions. |
| Model not found | Model 'openai/gpt-5-turbo' was not found by the provider. Please check the model name in the agent configuration. |
IonClaw agents can connect to external MCP servers using the built-in mcp_client tool. This lets agents use tools and read resources from any MCP-compatible server — other IonClaw instances, database servers, filesystem servers, or third-party services.
The mcp_client tool uses the MCP Streamable HTTP transport (JSON-RPC 2.0 over HTTP POST). It supports session management, authentication, and all standard MCP operations.
# 1. connect to a remote MCP server
mcp_client(action="initialize", url="http://localhost:9090/mcp")
# → returns session_id
# 2. list available tools
mcp_client(action="list_tools", url="http://localhost:9090/mcp", session_id="<id>")
# 3. call a tool
mcp_client(action="call_tool", url="http://localhost:9090/mcp", session_id="<id>", tool_name="chat", tool_arguments={"message": "Hello"})
# 4. close when done
mcp_client(action="close", url="http://localhost:9090/mcp", session_id="<id>")
| Action | Description |
|---|---|
initialize |
Connect to the server and negotiate a session |
list_tools |
List available tools on the remote server |
call_tool |
Call a specific tool by name with arguments |
list_resources |
List available resources |
read_resource |
Read a resource by URI |
ping |
Check if the server is alive |
close |
Close the session (sends HTTP DELETE) |
| Parameter | Type | Required | Description |
|---|---|---|---|
action |
string | Yes | Action to perform |
url |
string | Yes | MCP server endpoint URL |
session_id |
string | No | Session ID from initialize (if server provides one) |
auth_token |
string | No | Bearer token for authentication |
tool_name |
string | call_tool | Tool name to call |
tool_arguments |
object | No | Arguments for the tool |
resource_uri |
string | read_resource | Resource URI to read |
timeout |
integer | No | Timeout in seconds (default: 30) |
For complete usage examples, see the mcp-client skill.