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 agent to kagent’s MCP server, then discover and invoke your AgentInstances 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 AgentInstancesAgentInstanceA running, conversational pairing of a Harness and an AgentTemplate. Unlike the two, it is not a Kubernetes resource: kagent's gRPC API creates it and its 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 sub-agent.
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.
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: Streamable HTTP only. Server-Sent Events (SSE) as a standalone transport and stdio are both unsupported, so a client that offers a transport choice must use Streamable HTTP.
- 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/tasksextension, which changes how invocations behave. For more information, see Invoke without waiting.
Note
The tools take no session or conversation argument, because an AgentInstance is the conversation. Sending a second message to the same agent_instance_id continues where the first left off. The reply’s context_id names the durable conversation rather than the instance, so it differs from the agent_instance_id and is shared by any fork taken from it. To hold two independent conversations on one AgentTemplate, create two AgentInstances.
Warning
The open source build does not authenticate this endpoint. Every request is accepted, and the caller’s identity is read from an X-User-Id header that the caller sets itself, defaulting to admin@kagent.dev. Because the endpoint can invoke agents, create checkpoints, and create AgentInstances, do not expose port 8083 outside the cluster. For the wider identity model and what the open source build does guarantee, see Identity.
Before you begin
Install kagent, and confirm that your installation sets
controller.grpc.reflection=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 you have at least one AgentInstance in the
READYstate. The MCP server lists ready instances only.Install grpcurl. Deleting a checkpoint has neither an MCP tool nor a kagent command, so cleaning one up calls
CheckpointServicedirectly.
Connect a client
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.Add
--scope projectto limit the entry to the current project rather than your user configuration.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 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
Ask for the agents in the
kagentnamespace. The client callslist_agent_instancesand reports one line per instance.> List the kagent agents in the kagent namespace.An unexpectedly empty list is typically due to creator scoping rather than a missing agent. The tool returns only the instances that the calling identity created, and has no option to widen that scope. The kagent command line interface and this endpoint both default to
admin@kagent.dev, so they see each other’s instances. If you created the instance withkagent --user-id <someone-else>, send a matchingX-User-Idheader from the client.Ask the agent a question by naming the instance you want to use. The client calls
invoke_agent_instanceand fills in the arguments from the tool schema.> Ask kagent agent instance <agent-instance-id> in the kagent namespace: what is 2+2? Answer with just the number.Ask a follow-up that depends on the previous answer, such as
Multiply that by 10., against the same instance. The word “that” resolves only when the earlier turns are in context, because the transcript belongs to the AgentInstance rather than to the client.
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_agent_instance 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.
Checkpoint and fork
You can checkpoint an instance before letting an agent try something risky, then fork that checkpoint to start a second agent from the pinned state. The client calls the three checkpointCheckpointA durable pin on the snapshot that an AgentInstance 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 AgentInstance can be forked from it.Learn more tools, which give it the same operations that the Agent Substrate example performs from the command line.
Ask the client to checkpoint the instance.
> Checkpoint kagent agent instance <agent-instance-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.Ask the client to fork that checkpoint.
> Fork that checkpoint.The client reports a second AgentInstance with its own ID, already
READY, on the same Harness and AgentTemplate as the original.Ask the fork a question.
> Ask that fork what 10+5 is.The fork answers
15. Ask it about an earlier turn and it answers from the conversation it inherited, because a fork continues from the checkpoint rather than starting fresh.
The two AgentInstances share everything up to the checkpoint and nothing after it, because new turns append only to the branch that received them. A fork also runs the revisionRevisionThe compiled, immutable output of one Harness and AgentTemplate pairing, identified by a content digest. An AgentInstance runs the revision it was created from for its whole life, so editing either resource affects only instances created afterward. its checkpoint was taken on, so editing the AgentTemplate afterwards does not change what the fork runs.
You can now safely clean up these resources.
Use agents from curl
With curl you build each request yourself, so every field is visible: the tool name, its arguments, and the _meta 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 to fork a second agent from it. Every request is a tools/call to /mcp unless it names a tasks/ method, and none of them 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
List the ready AgentInstances in the namespace.
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_agent_instances", "arguments": {} } }'The reply carries one line per instance as text, plus the same data as structured content. Example output:
kagent/01a068e3-aeb6-7abc-8d6f-5ba9becd3143 (my-first-agent via my-first-harness)Save the ID of the instance that you want to use.
export INSTANCE_ID=<agent-instance-id>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.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_instance", "arguments": { "agent_instance_id": "'"$INSTANCE_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:
{ "agent_instance_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" }Send the same request again with
Multiply that by 10.as themessage. The word “that” resolves only when the earlier turns are in context. Note thatcontext_idis unchanged whiletask_idis new, so the second turn joined the first conversation instead of starting its own.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_instance", "arguments": { "agent_instance_id": "'"$INSTANCE_ID"'", "message": "Multiply that by 10." } } }'Example output:
{ "agent_instance_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_agent_instance 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.
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.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_instance", "arguments": { "agent_instance_id": "'"$INSTANCE_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, so pass it back verbatim rather than parsing it. 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. Carry the same_metadeclaration on every poll: a request that omits it is rejected with-32021 tasks capability required but not declared by clientrather than falling back to a blocking read.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
statusreportsworkingorcancelled, send the request from the previous step again, waiting the interval thatpollIntervalMssuggests between calls. A status ofinput_requiredmeans the agent is waiting on a person, which is explored in Answer an agent’s question.When the
statusreadscompleted, stop polling. Theresultfield holds the same content that a blocking call would have returned as bothcontentandstructuredContent. Example output:{ "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, so resultType alone 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, so 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.
Poll the paused task and read
inputRequests. It is keyed by request ID, and each entry is anelicitation/createcall whoserequestedSchemais the schema kagent built for the pause. Note the key, because answering needs 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. An elicitation result ofacceptsends the answers on, whiledeclineandcanceltell the agent that the person refused. An agent can adapt to a refusal rather than treating it as an error.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 carries no 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 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." }] } }
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.
Checkpoint and fork
The three checkpoint tools pin an instance’s state and start a second agent from it. None of them needs the tasks extension, so none carries _meta.
Create a checkpoint. The
request_idis an idempotency key, so repeating the call with the same value returns the checkpoint that the first call created rather than pinning a second one.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_agent_instance_checkpoint", "arguments": { "agent_instance_id": "'"$INSTANCE_ID"'", "request_id": "my-first-checkpoint" } } }'The result identifies the checkpoint and the turn it pinned. Example output:
{ "checkpoint": { "id": "01a06d19-540a-7040-befb-ec4499c96ff2", "agent_instance_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 instance’s checkpoints to confirm what you can fork from.
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_agent_instance_checkpoints", "arguments": { "agent_instance_id": "'"$INSTANCE_ID"'" } } }'The result is a
checkpointsarray of the same records, oldest first. Example output:{ "checkpoints": [ { "id": "01a0690f-5548-7935-b7ca-70919fc9c221", "agent_instance_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", "agent_instance_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" } ] }Fork the checkpoint into a second AgentInstance. This call takes
checkpoint_idrather than an instance ID, because the checkpoint already identifies the instance it was taken on.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_agent_instance", "arguments": { "checkpoint_id": "'"$CHECKPOINT_ID"'", "request_id": "my-first-fork" } } }'The result is a new AgentInstance with its own ID, already
READY, on the same Harness and AgentTemplate as the original. Example output:{ "agent_instance": { "id": "01a06d19-7eec-797b-87b8-7397a96b1544", "harness": "my-first-harness", "agent_template": "my-first-agent", "state": "AGENT_INSTANCE_STATE_READY" } }Invoke the fork with
invoke_agent_instanceand its new ID. Note thatcontext_idmatches the original’s rather than the fork’s own ID, because the fork continues the conversation that the checkpoint pinned. Example output:{ "agent_instance_id": "01a06d19-7eec-797b-87b8-7397a96b1544", "task_id": "01a06d19-80e7-7554-8288-377eda9e861b", "context_id": "ce5a10b8-7789-4ba7-8395-e60a339de763", "state": "TASK_STATE_COMPLETED", "text": "15" }
A fork runs the revisionRevisionThe compiled, immutable output of one Harness and AgentTemplate pairing, identified by a content digest. An AgentInstance runs the revision it was created from for its whole life, so editing either resource affects only instances created afterward. its checkpoint was taken on, so editing the AgentTemplate afterwards does not change what the fork runs. For what a checkpoint captures, why a checkpoint taken on a suspended instance is the forkable kind, and what a fork does and does not inherit, see Suspend and resume and the Agent Substrate example.
You can now safely clean up these resources.
Clean up
Delete the fork that you created. No MCP tool deletes an AgentInstance, so use the kagent command line interface.
kagent delete agent-instance <fork-agent-instance-id>Port-forward the controller’s gRPC port, and leave the command running.
CheckpointServicelistens there rather than on the HTTP port that serves MCP.kubectl port-forward -n kagent svc/kagent-controller 8083:8083Delete the checkpoint that you created.
grpcurl -plaintext \ -d '{"checkpointId":"<checkpoint-id>"}' \ localhost:8083 kagent.api.v1alpha1.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 both port-forwards with
Ctrl+C.
MCP tool reference
The server exposes five tools. Two cover discovery and conversation, and three expose the checkpointCheckpointA durable pin on the snapshot that an AgentInstance 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 AgentInstance can be forked from it.Learn more operations, so a client can pin and branch an agent’s state as well as talk to it. Every tool takes a namespace because an AgentInstance is scoped to one. No tool deletes an object, so removing an AgentInstance or a checkpoint means leaving MCP for the command line.
| Tool | Required arguments | What it does |
|---|---|---|
list_agent_instances | namespace | Lists the ready AgentInstances that the caller created. Takes match_labels, page_size, and page_token. |
invoke_agent_instance | namespace, agent_instance_id, message | Sends a message and returns the agent’s reply. Takes message_id for idempotency. |
create_agent_instance_checkpoint | namespace, agent_instance_id | Pins the conversation at a turn boundary. Takes request_id for idempotency. |
list_agent_instance_checkpoints | namespace, agent_instance_id | Lists that instance’s checkpoints. Takes page_size and page_token. |
fork_agent_instance | namespace, checkpoint_id | Creates a new AgentInstance from a checkpoint. Takes request_id for idempotency. |