McpAgent·续

import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

export class MyMCP extends McpAgent {
    server = new McpServer({
        name: "Demo",
        version: "1.0.0",
    });

initialState = {
        counter: 1,
    };

async init() {
        this.server.resource(`counter`, `mcp://resource/counter`, (uri) => {
            return {
                contents: [{ uri: uri.href, text: String(this.state.counter) }],
            };
        });

this.server.tool(
            "add",
            "Add to the counter, stored in the MCP",
            { a: z.number() },
            async ({ a }) => {
                this.setState({ ...this.state, counter: this.state.counter + a });

return {
                    content: [
                        {
                            type: "text",
                            text: String(`Added ${a}, total is now ${this.state.counter}`),
                        },
                    ],
                };
            },
        );
    }

onStateChanged(state) {
        console.log({ stateUpdate: state });
    }
}

src/index.tsts

import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

type State = { counter: number };

export class MyMCP extends McpAgent<Env, State, {}> {
    server = new McpServer({
        name: "Demo",
        version: "1.0.0",
    });

initialState: State = {
        counter: 1,
    };

async init() {
        this.server.resource(`counter`, `mcp://resource/counter`, (uri) => {
            return {
                contents: [{ uri: uri.href, text: String(this.state.counter) }],
            };
        });

this.server.tool(
            "add",
            "Add to the counter, stored in the MCP",
            { a: z.number() },
            async ({ a }) => {
                this.setState({ ...this.state, counter: this.state.counter + a });

return {
                    content: [
                        {
                            type: "text",
                            text: String(`Added ${a}, total is now ${this.state.counter}`),
                        },
                    ],
                };
            },
        );
    }

onStateChanged(state: State) {
        console.log({ stateUpdate: state });
    }
}

Elicitation on legacy servers

MCP elicitation ↗︎ lets a server request user input while handling another request, such as a tool call. The legacy path defines two modes:

  • Form mode collects structured, non-sensitive data through the client.
  • URL mode sends the user to an out-of-band interaction, such as third-party authorization or payment.

The client must advertise support for a mode before the server sends it.

Form mode

Call this.server.server.elicitInput() in a tool handler. Pass extra.requestId as relatedRequestId so the response returns on the stream for the originating tool call:

const result = await this.server.server.elicitInput(
    {
        mode: "form",
        message: "By how much do you want to increase the counter?",
        requestedSchema: {
            type: "object",
            properties: {
                amount: {
                    type: "number",
                    title: "Amount",
                    minimum: 1,
                    maximum: 100,
                },
            },
            required: ["amount"],
        },
    },
    { relatedRequestId: extra.requestId },
);

if (result.action !== "accept" || !result.content) {
    return { content: [{ type: "text", text: "Counter unchanged." }] };
}

const amount = Number(result.content.amount);
const result = await this.server.server.elicitInput(
    {
        mode: "form",
        message: "By how much do you want to increase the counter?",
        requestedSchema: {
            type: "object",
            properties: {
                amount: {
                    type: "number",
                    title: "Amount",
                    minimum: 1,
                    maximum: 100,
                },
            },
            required: ["amount"],
        },
    },
    { relatedRequestId: extra.requestId },
);

if (result.action !== "accept" || !result.content) {
    return { content: [{ type: "text", text: "Counter unchanged." }] };
}

const amount = Number(result.content.amount);

For backwards compatibility, form requests may omit mode: "form". The schema supports flat objects with primitive fields. Do not use form mode to request passwords, API keys, access tokens, payment credentials, or other secrets.

URL mode

Use URL mode for interactions that must happen outside the MCP client. The request includes a message, the URL, and a unique elicitationId:

const elicitationId = crypto.randomUUID();
const result = await this.server.server.elicitInput(
    {
        mode: "url",
        message: "Connect your account to continue.",
        url: `https://example.com/connect?elicitationId=${elicitationId}`,
        elicitationId,
    },
    { relatedRequestId: extra.requestId },
);

if (result.action !== "accept") {
    return { content: [{ type: "text", text: "Connection cancelled." }] };
}

return {
    content: [
        {
            type: "text",
            text: "Connection page opened. Complete it in your browser.",
        },
    ],
};
const elicitationId = crypto.randomUUID();
const result = await this.server.server.elicitInput(
    {
        mode: "url",
        message: "Connect your account to continue.",
        url: `https://example.com/connect?elicitationId=${elicitationId}`,
        elicitationId,
    },
    { relatedRequestId: extra.requestId },
);

if (result.action !== "accept") {
    return { content: [{ type: "text", text: "Connection cancelled." }] };
}

return {
    content: [
        {
            type: "text",
            text: "Connection page opened. Complete it in your browser.",
        },
    ],
};

For URL mode, accept means the user consented to open the URL. It does not mean the external interaction finished. The server may later send notifications/elicitation/complete with the same elicitationId.

Do not put secrets, personal information, or a pre-authenticated protected-resource URL in url. Production servers should use HTTPS. Bind each request to the authenticated user, and verify that the same user completes the external flow.

Handle responses

Both modes return one of three actions:

Action Meaning
accept The user submitted the form or consented to open the URL.
decline The user explicitly rejected the request.
cancel The user dismissed the request without making an explicit choice.

Accepted form responses include content that matches requestedSchema. URL responses omit content. Decline and cancel responses typically omit it.

switch (result.action) {
    case "accept":
        // For form mode, validate and process result.content.
        break;
    case "decline":
        return { content: [{ type: "text", text: "Request declined." }] };
    case "cancel":
        return { content: [{ type: "text", text: "Request dismissed." }] };
}
switch (result.action) {
    case "accept":
        // For form mode, validate and process result.content.
        break;
    case "decline":
        return { content: [{ type: "text", text: "Request declined." }] };
    case "cancel":
        return { content: [{ type: "text", text: "Request dismissed." }] };
}

MCP client support

Not all MCP clients implement legacy elicitation. Check the client before depending on it and provide a fallback when appropriate. Agents acting as MCP clients can handle both modes through MCP client elicitation handlers.

For more human-in-the-loop patterns, refer to Human-in-the-loop patterns.

Next steps

Build a Remote MCP server

Get started with MCP servers on Cloudflare.

MCP Tools

Design and add tools to your MCP server.

Authorization

Set up OAuth authentication.

Securing MCP servers

Security best practices for production.

createMcpHandler

Build stateless MCP servers.

Was this helpful?

YesNo

On this page

McpAgent·续 - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/model-context-protocol/apis/agent-api/#page","headline":"McpAgent","description":"Reference the deprecated, feature-frozen McpAgent class while migrating existing stateful MCP servers to stateless handlers.","url":"https://developers.cloudflare.com/agents/model-context-protocol/apis/agent-api/","inLanguage":"en","image":"https://developers.cloudflare.com/agents/model-context-protocol/apis/agent-api/og.png?v=d322518ac23269ff","dateModified":"2026-07-27","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["MCP"]}


description: Connect Agents to external MCP servers to use their tools, resources, and prompts over the Model Context Protocol. title: McpClient image: https://developers.cloudflare.com/agents/model-context-protocol/apis/client-api/og.png?v=d2c89cb40edacc61


Skip to content

Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt
Use this file to discover all available pages before exploring further.