For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
Use agents from an MCP client
Connect Claude Code, Cursor, or another MCP client to kagent’s MCP server, then discover your agents and invoke them as tools.
The kagent controller runs a Model Context ProtocolModel Context ProtocolAn open protocol for exposing tools and resources to a model. kagent reaches an MCP server through a RemoteMCPServer resource, and an AgentTemplate binds individual tools from it.Learn more (MCP) server that exposes your agents and their SessionsSessionA running conversation with one Agent. Unlike the resources it is built from, a Session is not a Kubernetes resource: kagent's gRPC API creates it and its PostgreSQL database tracks it.Learn more 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 subagent.
This example runs in the opposite direction to 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.
An MCP client such as Claude Code or Cursor builds each call. To see each call, or to drive the endpoint without a client, send raw curl requests.
About the kagent MCP server
The MCP server runs on the controller’s HTTP port, 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.
- State: The handler is stateless. Each request stands alone, and a client does not need to establish an MCP session first.
- Extensions: The server advertises the
io.modelcontextprotocol/tasksextension, which changes how invocations behave. For more information, see 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.
Before you begin
Install kagent, 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.Create your first agent, so that the cluster has an AgentAgentA Kubernetes custom resource that pairs one AgentTemplate with one Harness. Each side takes either an inline spec or a reference to an existing resource, and the controller compiles the pair into a revision.Learn more to invoke.
Connect a client
A client reaches the MCP server through a port-forward to the controller’s HTTP port. After the port-forward runs, register the endpoint in your client, or confirm the endpoint with a curl request.
Port-forward the controller’s HTTP port, and leave the command running.
kubectl port-forward -n kagent svc/kagent-controller 8083:8083Add
http://localhost:8083/mcpto your client.To limit the entry to the current project, add
--scope project.claude mcp add --transport http kagent http://localhost:8083/mcpContinue with the steps in Use agents from Claude Code or Cursor.
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. You discover the agents in your cluster, start a Session with one, answer the agent when it stops to ask you something, and checkpoint its Session.
List and invoke an agent
The list_agents tool returns the Agents that the caller can see. The invoke_agent tool sends one of them a message and starts a Session.
Ask for your agents. The client calls
list_agentsand reports the namespace and name of each Agent. A listed Agent is not necessarily ready, and the caller might lack permission to invoke it.> List my kagent agents.Ask the agent a question by naming the Agent. The client calls
invoke_agentwith the Agent’s namespace and name. The call starts a new Session, and the reply includes the Session ID.> Ask the kagent agent my-first-agent in the kagent namespace: what is 2+2? Answer with just the number.If your client declares the
io.modelcontextprotocol/tasksextension, the reply is a task ID instead, and the client polls the task in the background. A client declares its own capabilities on each request, so nothing on the server decides whether a call blocks.Ask a follow-up that depends on the previous answer, such as
Multiply that by 10., in the same Session. The client callsinvoke_agentagain with thesession_idfrom the first reply. 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_idcontinues where the first left off. The reply’scontext_idis the Session’s own ID. To hold two independent conversations with one Agent, invoke the Agent by name twice, which starts two Sessions.To continue a Session that already exists, ask the client to list your Sessions.
> List my kagent sessions.The
list_sessionstool returns only the Sessions that the calling identity created, and cannot widen that scope. The kagent command line interface (CLI) and this endpoint both default toadmin@kagent.dev, so they see each other’s Sessions. If you created the Session as another user withkagent --user-id <someone-else>, send a matchingX-User-Idheader from the client.
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 with the agent’s question and any choices that the agent offered. When you answer the prompt, the agent resumes the turn where it left off. When you refuse it, the agent is told that the person declined, and the agent can adapt to the refusal.
Only a client that declares the tasks extension can answer an agent. A blocking invoke_agent call cannot show the question, so an agent that pauses leaves that call waiting.
The pause is the same one that any other client sees, reached through MCP instead of Agent2Agent (A2A). For the pause kinds, the approval model, and what the agent receives, see Human in the loop.
Checkpoint a Session
Before an agent tries something risky, you can checkpoint its Session. The checkpointCheckpointA durable pin on the snapshot that a Session most recently suspended to, and a record of how far its transcript had advanced. Not a new state: tagging copies the snapshot so that Agent Substrate does not collect it, and a second Session can be forked from it.Learn more tools give the client the same checkpoint operations that are performed from the command line in the Agent Substrate example.
Ask the client to checkpoint the Session.
> 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 keeps that result in context, you can refer to the checkpoint without repeating its ID. To inspect or delete the checkpoint later, keep its ID.
You can now safely clean up these resources.
Use agents from curl
With curl you build each request yourself, so every field is visible, including 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, start a Session with one, answer the agent when it pauses, and checkpoint the Session. Every request is a tools/call to /mcp unless it names a tasks/ method, and no request needs an initialize handshake first.
Do not put io.modelcontextprotocol/protocolVersion in a request’s _meta. If a request includes it, the server requires a matching Mcp-Protocol-Version header and rejects the call without one. None of these requests need either field.
List and invoke an agent by using curl
Each request in this section is a blocking tools/call. The reply returns after the agent finishes the turn.
List the Agents that you can see. To limit the list to one namespace, add a
namespaceargument.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_agents", "arguments": {} } }'The reply contains an
agentsarray of namespace and name references, plus anext_page_tokenwhen more Agents remain. A listed Agent is not necessarily ready, and the caller might lack permission to invoke it. Example output:{ "agents": [ { "namespace": "kagent", "name": "my-first-agent" } ] }Start a Session by sending the Agent a message, and wait for the reply. The
agentargument takes a reference from thelist_agentsresult. A blocking call does not need_meta, because by default the tool does not require a client capability.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_agent", "arguments": { "agent": { "namespace": "kagent", "name": "my-first-agent" }, "message": "What is 2+2? Answer with just the number." } } }'The reply text comes back as the tool’s content, with the new Session’s ID and the task identifiers alongside it. Example output:
{ "session_id": "01a068e3-aeb6-7abc-8d6f-5ba9becd3143", "task_id": "01a06d0d-5fcf-7b07-aae3-1f470a8ee157", "context_id": "01a068e3-aeb6-7abc-8d6f-5ba9becd3143", "state": "TASK_STATE_COMPLETED", "text": "4" }Save the Session ID and the task ID from the reply. The checkpoint steps use the task ID.
export SESSION_ID=<session-id> export HEAD_TASK_ID=<task-id>Send
Multiply that by 10.to the same Session. This time,session_idreplacesagent, because the call takes exactly one of the two. The word “that” points back to the earlier answer. The model can follow the reference because the transcript belongs to the Session. In the reply,session_idandcontext_idare unchanged andtask_idis new, so the second turn continued the same Session. Save the new task ID.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_agent", "arguments": { "session_id": "'"$SESSION_ID"'", "message": "Multiply that by 10." } } }'Example output:
{ "session_id": "01a068e3-aeb6-7abc-8d6f-5ba9becd3143", "task_id": "01a06d0d-6864-79f1-a4cb-8547f77638ba", "context_id": "01a068e3-aeb6-7abc-8d6f-5ba9becd3143", "state": "TASK_STATE_COMPLETED", "text": "40" }export HEAD_TASK_ID=<task-id>
Invoke without waiting
Declaring the io.modelcontextprotocol/tasks extension changes the same tool’s result. Rather than blocking, invoke_agent 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, including each poll, repeats the declaration in its _meta. The tasks/cancel method stops a run that is still working, and the tasks/update method answers an agent that is waiting on a person.
Invoke the agent with the extension declared in
params._meta. If you omit the declaration, the tool blocks, as in List and invoke an agent by using curl.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_agent", "arguments": { "session_id": "'"$SESSION_ID"'", "message": "Count to three." }, "_meta": { "io.modelcontextprotocol/clientCapabilities": { "extensions": { "io.modelcontextprotocol/tasks": {} } } } } }'The task has an opaque ID of the form
v1.<encoded reference>that identifies the Session and the A2A task. Pass the ID back verbatim. The checkpoint tools do not accept this ID. Example output:{ "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" }Save the task ID. The value ends in base64, so quote it.
export TASK_ID='<task-id>'Poll the task with
tasks/get. Include the same_metadeclaration on every poll. The server rejects a poll that omits it with-32021 tasks capability required but not declared by client.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": {} } } } } }'If the
statusreportsworking, send thetasks/getrequest again after the interval thatpollIntervalMssuggests. On aworkingtask,statusMessagecontains the raw task record, because the agent has not produced text yet, so do not present it as progress text. A status ofinput_requiredmeans the agent is waiting on a person. To answer it, see Answer an agent’s question by using curl. A status ofcancelledmeans that atasks/cancelcall stopped the run, so stop polling.When the
statusreadscompleted, stop polling. Readstatus, notresultType, to decide that a run is over, because a paused task reportsresultTypeascompletewhile itsstatusis stillinput_required. Theresultfield contains the samecontentandstructuredContentthat a blocking call returns. A run that fails also reportscompleted, so readstructuredContent.stateto tell success from failure. Example output:{ "status": "completed", "statusMessage": "1, 2, 3", "resultType": "complete", "result": { "content": [{ "type": "text", "text": "1, 2, 3" }], "structuredContent": { "session_id": "01a068e3-aeb6-7abc-8d6f-5ba9becd3143", "task_id": "01a06d0d-6f2a-7c41-9b5e-31d0c2a7e8f4", "context_id": "01a068e3-aeb6-7abc-8d6f-5ba9becd3143", "state": "TASK_STATE_COMPLETED", "text": "1, 2, 3" } } }Save the
structuredContent.task_idvalue. The checkpoint steps use this task ID, not thev1.task ID.export HEAD_TASK_ID=<task-id>
Answer an agent’s question by using curl
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 the request as an MCP elicitation. To answer it, send a tasks/update call.
Poll the paused task and read
inputRequests.inputRequestsis keyed by request ID, and each entry is anelicitation/createcall whoserequestedSchemais the schema that kagent built for the pause. Note the key, because thetasks/updatecall requires it. Example output:{ "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
responsewhen the agent asks one question andresponse_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 withenum, 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.
- A question becomes one string field per question, named
Save the request ID that keys the entry.
export INPUT_REQUEST_ID=<input-request-id>Send the answer with
tasks/update, keyinginputResponsesby the same request ID. Anacceptresult sends the answers to the agent. Adeclineorcancelresult tells the agent that the person refused. If the response key does not match theinputRequestskey, the server rejects the response with anInvalid paramserror (-32602), and the task staysinput_required. Read the key fromtasks/get, and do not reuse a task ID or a checkpoint ID.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:
{ "resultType": "complete" }Poll the task again to collect the resumed turn. The agent picks up where it paused, so the status returns to
workingand thencompletedwith the answer that your response produced. Example output:{ "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." }] } }
For the pause kinds, the approval model, and what the agent receives, see Human in the loop.
Checkpoint a Session by using curl
The checkpoint tools pin a Session’s state. These tools do not need the tasks extension, so their requests omit _meta.
Create a checkpoint of the latest finished turn.
expected_head_task_ididentifies the turn to pin. If the Session has advanced past that turn, the call fails. Use the$HEAD_TASK_IDfrom the latest turn. Thev1.task ID thattasks/getuses fails withconversation advanced beyond the expected task.request_idis an idempotency key, so a repeated call with the same value returns the existing checkpoint.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": "'"$HEAD_TASK_ID"'", "request_id": "my-first-checkpoint" } } }'The result identifies the checkpoint and the turn that it pinned. Example output:
{ "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" } }Save the checkpoint ID.
export CHECKPOINT_ID=<checkpoint-id>List the Session’s checkpoints.
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
checkpointsarray of the same records, oldest first. Example output:{ "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" } ] }Start a second Session from the checkpoint.
request_idis an idempotency key, as in the checkpoint request. For what a fork replays and where it diverges, see Suspend and resume.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": 8, "method": "tools/call", "params": { "name": "fork_session", "arguments": { "checkpoint_id": "'"$CHECKPOINT_ID"'", "request_id": "my-first-fork" } } }'The result identifies the new Session, which is ready for a message. Example output:
{ "session": { "agent": "my-first-agent", "id": "01a06d1a-2b7c-7d40-8e13-5c9f04b1a6d2", "state": "RUNTIME_STATE_READY" } }
You can now safely clean up these resources.
Clean up
Delete the checkpoint that you created. If you used Claude Code or Cursor, set
CHECKPOINT_IDto the ID that the client reported.CheckpointServicelistens on the same port that serves MCP, so the port-forward from Connect a client already reaches the service.grpcurl -plaintext \ -d '{"checkpointId":"'"$CHECKPOINT_ID"'"}' \ localhost:8083 kagent.api.v1.CheckpointService/DeleteCheckpointRemove the server entry from your client. In Claude Code, run
claude mcp remove kagent. In Cursor, delete thekagententry from your MCP settings.Stop the port-forward with
Ctrl+C.
MCP tool reference
The server exposes the following agent and Session tools. Some cover discovery and conversation, and the rest expose the checkpointCheckpointA durable pin on the snapshot that a Session most recently suspended to, and a record of how far its transcript had advanced. Not a new state: tagging copies the snapshot so that Agent Substrate does not collect it, and a second Session can be forked from it.Learn more operations, so a client can pin an agent’s state as well as talk to it. An Agent is addressed by its namespace and name, and a Session by its own universally unique identifier (UUID). None of these tools deletes a Session or a checkpoint. To delete a Session, run kagent agent session delete. To delete a checkpoint, call the gRPC API directly, because the CLI has no checkpoint command. The server also exposes a set of standalone sandbox tools, which this example does not cover.
| Tool | Required arguments | What it does |
|---|---|---|
list_agents | None | Lists the Agents that the caller can see. Takes namespace to filter, and page_size and page_token. |
list_sessions | None | Lists the ready Sessions that the caller created. Takes page_size and page_token. |
invoke_agent | message, and exactly one of agent or session_id | Starts a new Session with agent, or continues one with session_id, and returns the agent’s reply with the Session and task IDs. Takes message_id for idempotency. |
create_session_checkpoint | session_id, expected_head_task_id | Pins the Session at a turn boundary. The call fails if the Session 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 Session’s transcript through the pinned turn. Takes request_id for idempotency. |