
This lesson is part of Learn MCP, our free course on the Model Context Protocol, within The MCP Guide.
In this lesson you will learn two official MCP extensions: Tasks, for work that takes longer than a normal tool call, and MCP Apps, for returning an interactive interface inside the chat. You will see how each one works, when to use it, how a client and server agree to use it, and why you must always keep a plain fallback.
This is the first lesson in the Advanced module. It builds on the tools you designed in Lesson 23 and the permissions work in Lesson 25. For a business-level overview first, read MCP Apps, Tasks and Skills explained.
Both features are extensions, not core protocol. As of October 2026, support varies by client, so you design for "maybe."
Key Takeaways
- Extensions are opt-in: both client and server must declare them in the
extensionsfield of their capabilities, and the server must fall back or return an error if the other side doesn't. - Tasks (
io.modelcontextprotocol/tasks) let a server return a durable task handle instead of blocking, so the client can poll withtasks/getand fetch the result later. - In the 2026-07-28 spec, tasks moved out of the core protocol into an official extension, with polling via
tasks/getand input viatasks/update. - MCP Apps (
io.modelcontextprotocol/ui) let a tool point to aui://HTML resource that the host renders in a sandboxed iframe. - Always return useful text content too, so clients without the extension still get an answer.
How extension negotiation works
The Versioning and Compatibility page explains that extensions are advertised in an extensions map inside capabilities. A server that supports Tasks advertises it like this in its server/discover result:
{
"capabilities": {
"tools": {},
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
}
The rule that matters most: "If one party supports an extension but the other does not, the supporting party MUST either revert to core protocol behavior or reject the request with an appropriate error." So every extension feature you build needs a core-protocol path.
Part 1: Long-running work with Tasks
Step 1: Decide whether you need a task
The Tasks overview lists good fits: long-running operations such as CI pipelines or batch jobs, human approval steps, external job systems that already use job IDs, unreliable connections and bulk processing.
It also explains why blocking is a poor choice. Many clients and intermediaries time out after a few seconds, and a task ID survives a disconnect or restart. If your tool reliably finishes in a second or two, you don't need Tasks.
Step 2: Learn the task lifecycle
A task moves through five statuses, per the overview:
| Status | Meaning |
|---|---|
working | In progress |
input_required | The server needs input, listed in inputRequests |
completed | Done; result holds what the call would have returned |
failed | error holds the JSON-RPC error |
cancelled | Cancelled (not always honored) |
The last three are terminal. Once there, a task never changes.
Step 3: Return a task handle
When a client has declared Tasks support and a request will take a while, the server returns a CreateTaskResult with resultType: "task" instead of the normal result. This example is adapted from the field list in the ext-tasks 2026-07-28 schema:
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"resultType": "task",
"taskId": "8f3c2a9e-1b7d-4c55-9a0e-3f6b2d1c7e44",
"status": "working",
"createdAt": "2026-10-28T14:00:00Z",
"lastUpdatedAt": "2026-10-28T14:00:00Z",
"ttlMs": 3600000,
"pollIntervalMs": 5000
}
}
The overview's server rules are short and strict:
- Advertise the extension in
server/discover. - Check the client declared support on this request. "Never return a task to a client that did not declare support."
- Create the task durably before you send the response.
- Serve
tasks/get, and includeresultorerrorfor terminal states. - Accept
tasks/updatefor input, and acknowledgetasks/cancel. Cancellation is cooperative.
Step 4: Secure your task IDs
A task ID works like the state handles from Lesson 25. The spec's Security Best Practices say servers must not treat possession of a handle as authentication. Generate task IDs with a secure random generator, store them keyed to the authenticated user, and reject tasks/get from anyone else. Set a sensible TTL so old results expire.
Step 5: Check SDK support
The ext-tasks repository publishes a TypeScript package, @modelcontextprotocol/ext-tasks. As of 28 October 2026, its README describes requester lifecycle APIs and receiver support for the 2025-11-25 version of tasks. Check your SDK's docs for 2026-07-28 server support before you plan a build around it. Our Tasks extension reference tracks changes.
Part 2: Interactive UI with MCP Apps
Step 6: Decide whether you need an app
The MCP Apps overview suggests apps for exploring complex data, configuring many options, and previewing rich media. A text answer is still better for simple questions. If a table in text does the job, skip the app.
Step 7: Understand how an app is wired
An MCP App combines two things you already know: a tool and a resource.
- The tool's description includes
_meta.ui.resourceUri, pointing to aui://resource. - The host fetches that resource, an HTML page that usually bundles its own JavaScript and CSS.
- The host renders it in a sandboxed iframe. The overview says apps "can't access the parent page, steal cookies, or escape their container."
- The app talks to the host over a postMessage JSON-RPC channel and can ask the host to call your server's tools, subject to the host's rules.
Here is the tool registration from the official Build an MCP App guide, shortened. Note that as of 28 October 2026 that guide imports the v1 TypeScript SDK (@modelcontextprotocol/sdk) alongside @modelcontextprotocol/ext-apps.
// The ui:// scheme tells hosts this is an MCP App resource.
const resourceUri = "ui://get-time/mcp-app.html";
registerAppTool(
server,
"get-time",
{
title: "Get Time",
description: "Returns the current server time.",
inputSchema: {},
_meta: { ui: { resourceUri } },
},
async () => {
const time = new Date().toISOString();
return {
content: [{ type: "text", text: time }],
};
},
);
The guide then calls registerAppResource to serve the bundled HTML at the same ui:// URI. Notice the tool still returns plain text. That is your fallback for hosts without app support.
Step 8: Apply app security basics
The sandbox protects the host, but you still own what the app does:
- Load external scripts only from origins you list in
_meta.ui.csp, and keep that list short. - Request extra permissions, such as camera or microphone, only if the feature needs them.
- Treat every tool call the app triggers like any other call: validate, authorize, rate limit and log.
Step 9: Check where apps render
The overview lists Claude, Claude Desktop, VS Code GitHub Copilot, Microsoft 365 Copilot, Goose, Postman, MCPJam and Archestra.AI as supporting MCP Apps, and points to the community-maintained client matrix. Test in at least one supporting host and one that doesn't support apps. See our MCP Apps reference for more.
Try it
- List three tools from your own server. For each, write "plain," "task" or "app" and one sentence why.
- For the "task" tool, write the
CreateTaskResultJSON it would return, with a realisticpollIntervalMs. - For the "app" tool, write the plain-text fallback a non-app client would show.
- Write down how you will stop one user from polling another user's task ID.
If all three tools came out "plain," that's a fine answer. Extensions add complexity, so use them only when they clearly help.
Recap
You learned how extensions are negotiated and why a fallback is required. You saw how Tasks turn slow work into a durable handle with a clear lifecycle, and how MCP Apps pair a tool with a sandboxed ui:// resource. Lesson 30 brings the whole course together in a capstone plan.
Course navigation
Previous lesson: Lesson 28: Publish to the MCP Registry.
Next lesson: Lesson 30: Capstone: Plan a Real Server.
Back to the Learn MCP course outline.
Frequently asked questions
Are Tasks part of the core MCP spec?
Not since 2026-07-28. The changelog moved experimental tasks out of core into the official io.modelcontextprotocol/tasks extension, replaced tasks/result with polling via tasks/get, added tasks/update and removed tasks/list.
What happens if a client doesn't support Tasks?
The server must not return a task to that client. It should do the work synchronously, if it can, or return an error that explains the limit.
Are progress notifications still useful?
Yes, for shorter operations. Tasks also carry status messages and can push updates through subscriptions/listen notifications, so pick whichever fits the job length. See Lesson 4 for the basic request flow.
Is an MCP App just a web page?
It is an HTML page, but it runs inside the host's sandbox and talks to your server through the host. That gives it the conversation context without its own login and API layer.
Do MCP Apps work in every AI app?
No. As of October 2026, support varies by host. Always return a plain text or structured result so every client gets an answer.











