> For the complete documentation index, see [llms.txt](/docs/llms.txt). Markdown versions of all docs pages are available by appending .md to any docs URL.

# Use agents from an MCP client

The kagent controller runs a Model Context Protocol (MCP) server that exposes your Sessions as tools. Any MCP client can then discover the agents in your cluster and delegate work to them. This mechanism allows one agent to orchestrate another as a sub-agent.

This example runs in the opposite direction to [Your first MCP tool](https://kagent.dev/docs/kagent/1.x/get-started/your-first-mcp-tool/). There, kagent is the MCP client and an external server provides the tools. Here, kagent is the MCP server and your agents are the tools.

You can do this work from an MCP client such as Claude Code or Cursor, which builds the calls for you, or with raw `curl` requests when you want to see those calls or drive the endpoint without a client.

## About the kagent MCP server

The MCP server is part of the controller's HTTP port rather than a separate deployment, so a default installation already serves it at `/mcp` on port `8083`.

- **Transport**: Clients connect over Streamable HTTP. Standalone Server-Sent Events (SSE) and stdio are unsupported.
- **Sessions**: The handler is stateless, so each request stands alone and a client does not need to establish a session first.
- **Extensions**: The server advertises the `io.modelcontextprotocol/tasks` extension, which changes how invocations behave. For more information, see [Invoke without waiting](#invoke-without-waiting).

> [!WARNING]
> A default installation sets `config.server.auth.mode` to `insecure`. In this mode the endpoint accepts every request and reads the caller's identity from an `X-User-Id` header that the caller sets itself, defaulting to `admin@kagent.dev`. The endpoint can invoke agents, create checkpoints, and create Sessions. Do not expose port `8083` outside the cluster. For the wider identity model, see [Identity](https://kagent.dev/docs/kagent/1.x/about/substrate-runtime/identity/).

## Before you begin

1. [Install kagent](https://kagent.dev/docs/kagent/1.x/setup/installation/), and confirm that your installation sets `config.server.grpcReflection=true`. Reflection lets a gRPC client discover the controller's methods without a local copy of kagent's protocol buffer definitions.
2. [Create your first agent](https://kagent.dev/docs/kagent/1.x/get-started/your-first-agent/), so that you have at least one Session in the `READY` state. The MCP server lists ready Sessions only.
3. Install [grpcurl](https://github.com/fullstorydev/grpcurl). Deleting a checkpoint has neither an MCP tool nor a kagent command. Cleaning one up calls `CheckpointService` directly.

## Connect a client

1. Port-forward the controller's HTTP port, and leave the command running.
   
   ```bash
   kubectl port-forward -n kagent svc/kagent-controller 8083:8083
   ```
2. Add `http://localhost:8083/mcp` to your client.
   
   You can choose from the following options.
   
   **Option: Claude Code**
   
   Add `--scope project` to limit the entry to the current project rather than your user configuration.
   
   ```bash
   claude mcp add --transport http kagent http://localhost:8083/mcp
   ```
   
   Continue with the steps in [Use agents from Claude Code or Cursor](#use-agents-from-claude-code-or-cursor).
   
   **Option: Cursor**
   
   Add the server to your Cursor MCP settings.
   
   ```json
   {
     "mcpServers": {
       "kagent": {
         "url": "http://localhost:8083/mcp"
       }
     }
   }
   ```
   
   Continue with the steps in [Use agents from Claude Code or Cursor](#use-agents-from-claude-code-or-cursor).
   
   **Option: curl**
   
   Nothing needs to be registered, because each request stands alone. Confirm the endpoint before you continue. Streamable HTTP replies are framed as events, so each response arrives on a `data:` line.
   
   ```bash
   curl -s -X POST http://localhost:8083/mcp \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json, text/event-stream' \
     -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
   ```
   
   Example output:
   
   ```console
   event: message
   data: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"extensions":{"io.modelcontextprotocol/tasks":{}},"tools":{"listChanged":true}},"protocolVersion":"2025-06-18","serverInfo":{"name":"kagent","version":"v1.0.0"}}}
   ```
   
   Continue with the steps in [Use agents from curl](#use-agents-from-curl).

## Use agents from Claude Code or Cursor

Claude Code and Cursor read the tool schemas and build each call, so you work in plain language rather than JSON. You discover the agents in your cluster, hold a conversation with one, answer the agent when it stops to ask you something, and pin its state so that a second agent can start from that point.

### List and invoke an agent

1. Ask for your conversations. The client calls `list_sessions` and reports one line per Session.
   
   ```console
   > List my kagent sessions.
   ```
   
   The tool returns only the Sessions that the calling identity created, and cannot widen that scope. An empty list therefore means that this identity has not created a Session. The kagent command line interface and this endpoint both default to `admin@kagent.dev`, so they see each other's Sessions. When you created the Session as another user with `kagent --user-id <someone-else>`, send a matching `X-User-Id` header from the client.
2. Ask the agent a question by naming the Session you want to use. The client calls `invoke_session` and fills in the arguments from the tool schema.
   
   ```console
   > Ask kagent session <session-id>: what is 2+2?
     Answer with just the number.
   ```
3. Ask a follow-up that depends on the previous answer, such as `Multiply that by 10.`, against the same Session. The word "that" points back to the earlier answer. The model can follow the reference because the transcript belongs to the Session rather than to the client.
   
   Because a Session *is* one conversation, a second message to the same `session_id` continues where the first left off. The reply's `context_id` is the Session's own ID. To hold two independent conversations with one Agent, create two Sessions.

> [!NOTE]
> Nothing here configures whether an invocation blocks or returns a task to poll, because a client declares its own capabilities on each request. A reply means your client did not declare the `io.modelcontextprotocol/tasks` extension and the call waited for the agent to finish. A task ID means it did, and the client polls in the background so that a long agent run never holds a request open.

### Answer an agent's question

When an agent pauses to ask something, kagent returns the question as an MCP elicitation and your client presents its own prompt: the agent's question, and a fixed set of choices where the agent offered them. When you answer it, the agent resumes the turn where it left off. When you refuse it, the agent is told that the person declined, which it can adapt to rather than treating it as an error.

> [!IMPORTANT]
> Only a client that declares the tasks extension can answer an agent. A blocking `invoke_session` call has nowhere to surface the question, so an agent that pauses leaves that call waiting.

This is the same pause that any other client sees, reached through MCP instead of A2A. For the pause types, the approval model, and what the agent receives, see [Human in the loop](https://kagent.dev/docs/kagent/1.x/agents/human-in-the-loop/).

### Checkpoint a Session

You can checkpoint a Session before letting an agent try something risky. The client calls the checkpoint tools, which give the client the same checkpoint operations that the [Agent Substrate example](https://kagent.dev/docs/kagent/1.x/examples/agent-substrate/) performs from the command line.

1. Ask the client to checkpoint the Session.
   
   ```console
   > Checkpoint kagent session <session-id>.
   ```
   
   The client reports the checkpoint's own ID, the turn that it pinned, and a state of `CHECKPOINT_STATE_READY`. Because the client holds that result in context, you can refer to the checkpoint without repeating its ID.
2. Keep the checkpoint ID if you need to inspect or delete the checkpoint later.

You can now safely [clean up these resources](#clean-up).

## Use agents from curl

With `curl` you build each request yourself, so every field is visible: the tool name, its arguments, and the `_meta` field that decides whether a call blocks or returns a task to poll. You list the agents in your cluster, hold a conversation with one, answer the agent when it pauses, and checkpoint an instance. Every request is a `tools/call` to `/mcp` unless it names a `tasks/` method, and no request needs an `initialize` handshake first.

> [!NOTE]
> Do not put `io.modelcontextprotocol/protocolVersion` in a request's `_meta`. The server then requires a matching `Mcp-Protocol-Version` header and rejects the call without one. Neither field is necessary for any of these requests.

### List and invoke an agent

1. List your ready Sessions.
   
   ```bash
   curl -s -X POST http://localhost:8083/mcp \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json, text/event-stream' \
     -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "list_sessions",
         "arguments": {}
       }
     }'
   ```
   
   The reply carries one line per Session as text, plus the same data as structured content. Example output:
   
   ```console
   01a068e3-aeb6-7abc-8d6f-5ba9becd3143 (my-first-agent)
   ```
2. Save the ID of the Session that you want to use.
   
   ```bash
   export SESSION_ID=<session-id>
   ```
3. Send a message and wait for the reply. A blocking call needs no `_meta`, because the tool's default behavior asks nothing of the client.
   
   ```bash
   curl -s -X POST http://localhost:8083/mcp \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json, text/event-stream' \
     -d '{
       "jsonrpc": "2.0",
       "id": 2,
       "method": "tools/call",
       "params": {
         "name": "invoke_session",
         "arguments": {
           "session_id": "'"$SESSION_ID"'",
           "message": "What is 2+2? Answer with just the number."
         }
       }
     }'
   ```
   
   The reply text comes back as the tool's content, with the task identifiers alongside it. Example output:
   
   ```json
   {
     "session_id": "01a068e3-aeb6-7abc-8d6f-5ba9becd3143",
     "task_id": "01a06d0d-5fcf-7b07-aae3-1f470a8ee157",
     "context_id": "ce5a10b8-7789-4ba7-8395-e60a339de763",
     "state": "TASK_STATE_COMPLETED",
     "text": "4"
   }
   ```
4. Send the same request again with `Multiply that by 10.` as the `message`. The word "that" points back to the earlier answer. The model can follow the reference because the transcript belongs to the Session. Note that `context_id` is unchanged while `task_id` is new, so the second turn joined the first conversation instead of starting its own.
   
   ```bash
   curl -s -X POST http://localhost:8083/mcp \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json, text/event-stream' \
     -d '{
       "jsonrpc": "2.0",
       "id": 2,
       "method": "tools/call",
       "params": {
         "name": "invoke_session",
         "arguments": {
           "session_id": "'"$SESSION_ID"'",
           "message": "Multiply that by 10."
         }
       }
     }'
   ```
   
   Example output:
   
   ```json
   {
     "session_id": "01a068e3-aeb6-7abc-8d6f-5ba9becd3143",
     "task_id": "01a06d0d-6864-79f1-a4cb-8547f77638ba",
     "context_id": "ce5a10b8-7789-4ba7-8395-e60a339de763",
     "state": "TASK_STATE_COMPLETED",
     "text": "40"
   }
   ```

### Invoke without waiting

Declaring the `io.modelcontextprotocol/tasks` extension changes the same tool's result. Rather than blocking, `invoke_session` returns immediately with a task to poll, which keeps a long agent run from holding a request open. Because the handler is stateless, every request repeats the declaration in its `_meta` rather than establishing it once, and that includes each poll.

1. Invoke the agent with the extension declared in `params._meta`. Leave the declaration out and the tool blocks instead, as in [List and invoke an agent](#list-and-invoke-an-agent-1).
   
   ```bash
   curl -s -X POST http://localhost:8083/mcp \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json, text/event-stream' \
     -d '{
       "jsonrpc": "2.0",
       "id": 3,
       "method": "tools/call",
       "params": {
         "name": "invoke_session",
         "arguments": {
           "session_id": "'"$SESSION_ID"'",
           "message": "Count to three."
         },
         "_meta": {
           "io.modelcontextprotocol/clientCapabilities": {
             "extensions": { "io.modelcontextprotocol/tasks": {} }
           }
         }
       }
     }'
   ```
   
   The task carries an opaque ID of the form `v1.<encoded reference>` that identifies the namespace, the instance, and the A2A task together. Pass it back verbatim rather than parsing it. Example output:
   
   ```json
   {
     "taskId": "v1.eyJuYW1lc3BhY2UiOiJrYWdlbnQiLCJpbnN0YW5jZUlkIjoi...",
     "status": "working",
     "createdAt": "2026-09-04T15:35:25.722159606Z",
     "lastUpdatedAt": "2026-09-04T15:35:25.722159606Z",
     "ttlMs": null,
     "pollIntervalMs": 1000,
     "resultType": "task"
   }
   ```
2. Save the task ID. The value ends in base64, so quote it.
   
   ```bash
   export TASK_ID='<task-id>'
   ```
3. Poll the task with `tasks/get`. Carry the same `_meta` declaration on every poll: a request that omits it is rejected with `-32021 tasks capability required but not declared by client` rather than falling back to a blocking read.
   
   ```bash
   curl -s -X POST http://localhost:8083/mcp \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json, text/event-stream' \
     -d '{
       "jsonrpc": "2.0",
       "id": 4,
       "method": "tasks/get",
       "params": {
         "taskId": "'"$TASK_ID"'",
         "_meta": {
           "io.modelcontextprotocol/clientCapabilities": {
             "extensions": { "io.modelcontextprotocol/tasks": {} }
           }
         }
       }
     }'
   ```
4. If the `status` reports `working` or `cancelled`, send the request from the previous step again, waiting the interval that `pollIntervalMs` suggests between calls. A status of `input_required` means the agent is waiting on a person, which is explored in [Answer an agent's question](#answer-an-agents-question-1).
5. When the `status` reads `completed`, stop polling. The `result` field holds the same content that a blocking call would have returned as both `content` and `structuredContent`. Example output:
   
   ```json
   {
     "status": "completed",
     "statusMessage": "1, 2, 3.",
     "resultType": "complete",
     "result": {
       "content": [{ "type": "text", "text": "1, 2, 3." }]
     }
   }
   ```

Two more methods complete the set. `tasks/cancel` stops a run that is still working, and `tasks/update` answers an agent that is waiting on a person.

> [!WARNING]
> Read `status` rather than `resultType` to decide that a run is over. A paused task reports `resultType` as `complete` while its `status` is still `input_required`. `resultType` alone therefore does not mean an answer is waiting.

> [!NOTE]
> `statusMessage` changes meaning with the status. On a `working` task it holds the raw task record rather than a readable sentence, because the agent has not produced any text yet. Do not present it to a person as progress text. On an `input_required` task it holds the agent's question, and on a `completed` task it holds the reply.

### Answer an agent's question

When an agent pauses to ask something, the task's status becomes `input_required` and `tasks/get` returns an `inputRequests` object describing what the agent needs. kagent builds that as an MCP elicitation, and answering it is a `tasks/update` call.

1. Poll the paused task and read `inputRequests`. It is keyed by request ID, and each entry is an `elicitation/create` call whose `requestedSchema` is the schema kagent built for the pause. Note the key, because answering needs it. Example output:
   
   ```json
   {
     "status": "input_required",
     "statusMessage": "Which database should we use?",
     "inputRequests": {
       "01a06d12-4832-7e1f-873d-c25bc6b6b70b": {
         "method": "elicitation/create",
         "params": {
           "mode": "form",
           "message": "Which database should we use?",
           "requestedSchema": {
             "type": "object",
             "properties": {
               "response": {
                 "type": "string",
                 "description": "Which database should we use?",
                 "enum": ["PostgreSQL", "MySQL"]
               }
             },
             "required": ["response"],
             "additionalProperties": false
           }
         }
       }
     }
   }
   ```
   
   The schema's shape depends on what the agent asked for.
   
   - **A question** becomes one string field per question, named `response` when the agent asks one question and `response_1`, `response_2`, and so on when it asks more than one. A question with a fixed set of choices restricts the field to those values with `enum`, and one that accepts more than one answer takes an array.
   - **A tool approval** becomes one boolean field per tool, named `approve_1`, `approve_2`, and so on. A response must decide every tool in the request.
2. Save the request ID that keys the entry.
   
   ```bash
   export INPUT_REQUEST_ID=<input-request-id>
   ```
3. Send the answer with `tasks/update`, keying `inputResponses` by the same request ID. An elicitation result of `accept` sends the answers on, while `decline` and `cancel` tell the agent that the person refused. An agent can adapt to a refusal rather than treating it as an error.
   
   ```bash
   curl -s -X POST http://localhost:8083/mcp \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json, text/event-stream' \
     -d '{
       "jsonrpc": "2.0",
       "id": 5,
       "method": "tasks/update",
       "params": {
         "taskId": "'"$TASK_ID"'",
         "inputResponses": {
           "'"$INPUT_REQUEST_ID"'": {
             "action": "accept",
             "content": { "response": "PostgreSQL" }
           }
         },
         "_meta": {
           "io.modelcontextprotocol/clientCapabilities": {
             "extensions": { "io.modelcontextprotocol/tasks": {} }
           }
         }
       }
     }'
   ```
   
   The reply confirms only that the update was accepted, and does not include agent output. Example output:
   
   ```json
   { "resultType": "complete" }
   ```
4. Poll the task again to collect the resumed turn. The agent picks up where it paused, so the status returns to `working` and then `completed` with the answer your response produced. Example output:
   
   ```json
   {
     "status": "completed",
     "statusMessage": "You chose PostgreSQL as the database to use.",
     "resultType": "complete",
     "result": {
       "content": [{ "type": "text", "text": "You chose PostgreSQL as the database to use." }]
     }
   }
   ```

> [!NOTE]
> A response whose key does not match the `inputRequests` key is rejected with an `Invalid params` error (`-32602`) naming the key that the task expects, and the task stays `input_required`. A value that is not an elicitation result is rejected the same way. Read the key from `tasks/get` rather than reusing a task ID or a checkpoint ID.

For the pause types, the approval model, and what the agent receives, see [Human in the loop](https://kagent.dev/docs/kagent/1.x/agents/human-in-the-loop/).

### Checkpoint a Session

The checkpoint tools pin a Session's state. These tools do not need the tasks extension, so they do not carry `_meta`.

1. Create a checkpoint. `expected_head_task_id` names the finished turn to pin. When the conversation has advanced past that turn, the call fails rather than pinning the wrong one. Use the `$TASK_ID` that an earlier invocation returned. `request_id` is an idempotency key. Repeating the call with the same value returns the checkpoint that the earlier call created rather than pinning a second one.
   
   ```bash
   curl -s -X POST http://localhost:8083/mcp \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json, text/event-stream' \
     -d '{
       "jsonrpc": "2.0",
       "id": 6,
       "method": "tools/call",
       "params": {
         "name": "create_session_checkpoint",
         "arguments": {
           "session_id": "'"$SESSION_ID"'",
           "expected_head_task_id": "'"$TASK_ID"'",
           "request_id": "my-first-checkpoint"
         }
       }
     }'
   ```
   
   The result identifies the checkpoint and the turn it pinned. Example output:
   
   ```json
   {
     "checkpoint": {
       "id": "01a06d19-540a-7040-befb-ec4499c96ff2",
       "session_id": "01a068e3-aeb6-7abc-8d6f-5ba9becd3143",
       "head_task_id": "01a06d13-2504-7245-9eaf-9c1870c51d26",
       "history_sequence": 398,
       "state": "CHECKPOINT_STATE_READY",
       "created_at": "2026-09-04T15:46:11.594634Z"
     }
   }
   ```
2. Save the checkpoint ID.
   
   ```bash
   export CHECKPOINT_ID=<checkpoint-id>
   ```
3. List the Session's checkpoints.
   
   ```bash
   curl -s -X POST http://localhost:8083/mcp \
     -H 'Content-Type: application/json' \
     -H 'Accept: application/json, text/event-stream' \
     -d '{
       "jsonrpc": "2.0",
       "id": 7,
       "method": "tools/call",
       "params": {
         "name": "list_session_checkpoints",
         "arguments": {
           "session_id": "'"$SESSION_ID"'"
         }
       }
     }'
   ```
   
   The result is a `checkpoints` array of the same records, oldest first. Example output:
   
   ```json
   {
     "checkpoints": [
       {
         "id": "01a0690f-5548-7935-b7ca-70919fc9c221",
         "session_id": "01a068e3-aeb6-7abc-8d6f-5ba9becd3143",
         "head_task_id": "01a0690f-058d-7d29-a880-9b5d6d30b772",
         "history_sequence": 91,
         "state": "CHECKPOINT_STATE_READY",
         "created_at": "2026-09-03T20:56:47.689204Z"
       },
       {
         "id": "01a06d19-540a-7040-befb-ec4499c96ff2",
         "session_id": "01a068e3-aeb6-7abc-8d6f-5ba9becd3143",
         "head_task_id": "01a06d13-2504-7245-9eaf-9c1870c51d26",
         "history_sequence": 398,
         "state": "CHECKPOINT_STATE_READY",
         "created_at": "2026-09-04T15:46:11.594634Z"
       }
     ]
   }
   ```

To start a second Session from a checkpoint, call `fork_session` with the checkpoint ID. For what a fork replays and where it diverges, see [Suspend and resume](https://kagent.dev/docs/kagent/1.x/about/substrate-runtime/suspend-and-resume#checkpoints) and the [Agent Substrate example](https://kagent.dev/docs/kagent/1.x/examples/agent-substrate/).

You can now safely [clean up these resources](#clean-up).

## Clean up

1. Delete the checkpoint that you created. `CheckpointService` listens on the same port that serves MCP, so the port-forward from [Connect a client](#connect-a-client) already reaches the service.
   
   ```bash
   grpcurl -plaintext \
     -d '{"checkpointId":"<checkpoint-id>"}' \
     localhost:8083 kagent.api.v1.CheckpointService/DeleteCheckpoint
   ```
2. Remove the server entry from your client. In Claude Code, run `claude mcp remove kagent`. In Cursor, delete the `kagent` entry from your MCP settings.
3. Stop the port-forward with `Ctrl+C`.

## MCP tool reference

The server exposes five Session tools. Two cover discovery and conversation. Three expose the checkpoint operations, so a client can pin an agent's state as well as talk to it. No tool takes a namespace. A Session is addressed by its own UUID. No tool deletes an object either. Deleting a Session takes the `kagent agent session delete` command, and deleting a checkpoint takes a direct call to the gRPC API, because the CLI has no checkpoint command. The server also exposes a set of [standalone sandbox](https://kagent.dev/docs/kagent/1.x/about/substrate-runtime/standalone-sandboxes/) tools, which this example does not cover.

| Tool | Required arguments | What it does |
| --- | --- | --- |
| `list_sessions` | None | Lists the ready Sessions that the caller created. Takes `page_size` and `page_token`. |
| `invoke_session` | `session_id`, `message` | Sends a message and returns the agent's reply. Takes `message_id` for idempotency. |
| `create_session_checkpoint` | `session_id`, `expected_head_task_id` | Pins the conversation at a turn boundary. The call fails if the conversation has advanced past that task. Takes `request_id` for idempotency. |
| `list_session_checkpoints` | `session_id` | Lists that Session's checkpoints. Takes `page_size` and `page_token`. |
| `fork_session` | `checkpoint_id` | Starts a second Session from a checkpoint, replaying the conversation through the pinned turn. Takes `request_id` for idempotency. |

## Next steps

[Agent Substrate](https://kagent.dev/docs/kagent/1.x/examples/agent-substrate/)

Watch the Actor beneath an agent suspend between turns and resume on demand.

[Human in the loop](https://kagent.dev/docs/kagent/1.x/agents/human-in-the-loop/)

Understand the pauses that an agent can raise and how a client answers them.

[Your first MCP tool](https://kagent.dev/docs/kagent/1.x/get-started/your-first-mcp-tool/)

Point kagent at an MCP server so that your agent gains tools of its own.
