# Long-running MCP calls (tasks)

Call Liyagent's own /mcp tools as MCP tasks, poll and cancel them, and answer the questions a task asks while it runs.

Liyagent serves its own tools at `/mcp` (Streamable HTTP), for MCP clients such as Claude Desktop, Cursor or your own agents. Some calls take minutes. The longest is `ask_agent`, which runs a whole agent. Liyagent supports the **tasks** utility from MCP 2025-11-25 for these calls: the client gets a task at once, then polls for it and collects the result when it is ready.

A call without a task is answered exactly as before. Clients that do not use tasks see no change.

## Start a task

`initialize` advertises `"tasks": {"list": {}, "cancel": {}, "requests": {"tools": {"call": {}}}}`, and every tool in `tools/list` carries `"execution": {"taskSupport": "optional"}`. To run a call as a task, add `task` to its params:

```json
{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
 "params": {"name": "ask_agent", "arguments": {"agent": "it-helpdesk", "message": "..."},
            "task": {"ttl": 3600000}}}
```

The answer is a `CreateTaskResult`: `{"task": {"taskId", "status": "working", "createdAt", "lastUpdatedAt", "ttl", "pollInterval"}}`. The call has already passed the same checks as any other call: kill switch, rate limits, Cedar, budget, data policies and guardrails. They apply to the work the task runs.

| Method | What it does |
| --- | --- |
| `tasks/get` | The task's status: `working`, `input_required`, `completed`, `failed` or `cancelled` |
| `tasks/result` | The tool's result once the task ends, tagged `_meta["io.modelcontextprotocol/related-task"]`. With `Accept: text/event-stream` it streams until then, including any question the task asks. Without a stream it waits up to `mcp_task_result_wait_s` (30 s) |
| `tasks/list` | Your tasks, 50 at a time (`nextCursor`) |
| `tasks/cancel` | Ends the task as `cancelled` and stops the agent run it wraps at its next step. A task that already ended answers `-32602` |

A tool result with `isError` ends the task as `failed`.

## Answer a question mid-task

When a server that the asked agent calls needs something from the user (an MCP `elicitation/create`), the task moves to `input_required`. Call `tasks/result` with `Accept: text/event-stream`. The stream carries the request:

```json
{"jsonrpc": "2.0", "id": "tiq-task-<taskId>-1", "method": "elicitation/create",
 "params": {"mode": "form", "message": "Which cost centre?", "requestedSchema": {...},
            "_meta": {"io.modelcontextprotocol/related-task": {"taskId": "<taskId>"}}}}
```

POST your answer back to `/mcp` as a JSON-RPC response with the same `id`: `{"jsonrpc": "2.0", "id": "tiq-task-<taskId>-1", "result": {"action": "accept", "content": {...}}}`. The task then continues and the stream ends with the result.

The same rules apply as when a person answers in the console. A form that asks for a password, token, key or card number is declined without being shown to you. A URL must be public HTTPS. Your answer is checked against the form before the server sees it, and an answer that does not fit is a decline. No answer within `mcp_task_input_timeout_s` (10 minutes) is also a decline.

## Who can see a task

A task belongs to the caller that started it: the agent credential, or the app and the person signed in to it, in their tenant. Any other caller gets `-32602` for that task id, as if it did not exist, and cannot answer its questions. `tasks/list` lists only your own.

## Limits and durability

- Tasks are stored in the database, so any replica answers `tasks/get`. The work runs in the process that accepted the call and reports that it is alive every 10 seconds. If that process stops, the task is reported as `failed` ("interrupted"), not left `working`.
- `mcp_task_max_ttl_ms` caps how long a task is kept (default 24 hours). `mcp_task_max_open` caps a caller's open tasks (default 20). A call over the cap is refused with `refused_by: task_limit`.
- `mcp_tasks_enabled` turns tasks off.
- The audit log records `mcp.task.created`, `.completed`, `.failed`, `.cancelled`, `.input_required` and `.input_answered` under the caller's name. Answers are never recorded.
