McpClient·续1

for (const tool of tools) { console.log(Tool: ${tool.name}); console.log(From server: ${tool.serverId}); console.log(Title: ${tool.title ?? tool.annotations?.title ?? tool.name}); console.log(Description: ${tool.description}); }


`getMcpServers().tools` returns the same raw tool records as part of the full MCP client state. Neither API converts the tool schemas.

#### Integration with AI SDK

To use MCP tools with the AI SDK, use `this.mcp.getAITools()` which converts MCP tools to AI SDK format:

```js
import { generateText } from "ai";
import { createAI } from "agents/models/ai-sdk";

export class MyAgent extends Agent {
    async onRequest(request) {
        const ai = createAI({ binding: this.env.AI });
        const response = await generateText({
            model: ai("@cf/zai-org/glm-4.7-flash"),
            prompt: "What's the weather in San Francisco?",
            tools: this.mcp.getAITools(),
        });

return new Response(response.text);
    }
}
import { generateText } from "ai";
import { createAI } from "agents/models/ai-sdk";

export class MyAgent extends Agent<Env> {
    async onRequest(request: Request) {
        const ai = createAI({ binding: this.env.AI });
        const response = await generateText({
            model: ai("@cf/zai-org/glm-4.7-flash"),
            prompt: "What's the weather in San Francisco?",
            tools: this.mcp.getAITools(),
        });

return new Response(response.text);
    }
}

Note

Use this.mcp.listTools() or getMcpServers().tools for discovery. Call this.mcp.getAITools() when passing tools to the AI SDK. getAITools() converts each input and output schema to Zod, then reuses those schemas while the live connection keeps the same current catalog.

Resources and prompts

const state = this.getMcpServers();

// Available resources
for (const resource of state.resources) {
    console.log(`Resource: ${resource.name} (${resource.uri})`);
}

// Available prompts
for (const prompt of state.prompts) {
    console.log(`Prompt: ${prompt.name}`);
}
const state = this.getMcpServers();

// Available resources
for (const resource of state.resources) {
    console.log(`Resource: ${resource.name} (${resource.uri})`);
}

// Available prompts
for (const prompt of state.prompts) {
    console.log(`Prompt: ${prompt.name}`);
}

Elicitation

MCP servers can request user input while handling another operation. On the stateless path, elicitation returns input_required and completes through multi-round-trip requests (MRTR). On the legacy path, the server sends pushed elicitation/create requests. Both use form and URL modes.

Register a handler for each mode your Agent supports in onStart(). The same handlers serve both lanes:

import { Agent } from "agents";

class MyAgent extends Agent {
    onStart() {
        this.mcp.configureElicitationHandlers({
            form: (request, serverId) =>
                this.forwardElicitationToBrowser(request, serverId),
            url: (request, serverId) =>
                this.forwardElicitationToBrowser(request, serverId),
        });
    }

forwardElicitationToBrowser(request, serverId) {
        // Forward the request to your UI and resolve after the user responds.
        // A complete implementation appears in Forward elicitation to a UI.
        throw new Error(
            `Implement elicitation for ${serverId}: ${request.params.message}`,
        );
    }
}
import { Agent } from "agents";
import type { ElicitRequest, ElicitResult } from "agents/mcp/client";

class MyAgent extends Agent<Env> {
    onStart() {
        this.mcp.configureElicitationHandlers({
            form: (request, serverId) =>
                this.forwardElicitationToBrowser(request, serverId),
            url: (request, serverId) =>
                this.forwardElicitationToBrowser(request, serverId),
        });
    }

private forwardElicitationToBrowser(
        request: ElicitRequest,
        serverId: string,
    ): Promise<ElicitResult> {
        // Forward the request to your UI and resolve after the user responds.
        // A complete implementation appears in Forward elicitation to a UI.
        throw new Error(
            `Implement elicitation for ${serverId}: ${request.params.message}`,
        );
    }
}

The serverId identifies the connection that sent the request. Use it to tell the user which server is asking for input and to apply server-specific policy.

Capability negotiation and hibernation

The SDK advertises only the modes with configured handlers. Connections on the legacy path advertise them during initialize. Requests on the stateless path carry them with request capabilities. A form-only handler advertises form mode. A URL-only handler advertises URL mode. A connection without handlers advertises no elicitation capability, which lets the server use its fallback.

The SDK stores the advertised modes with each server registration. A connection restored after Durable Object hibernation can therefore advertise the same modes when it reconnects. Callback functions remain in memory and reattach when onStart() runs.

You can explicitly narrow the advertised modes when adding a server:

await this.addMcpServer("portal", "https://portal.example.com/mcp", {
    client: {
        capabilities: {
            elicitation: { form: {} },
        },
    },
});
await this.addMcpServer("portal", "https://portal.example.com/mcp", {
    client: {
        capabilities: {
            elicitation: { form: {} },
        },
    },
});

An explicit client.capabilities.elicitation value takes precedence over handler-derived modes and persists with the server registration. Do not advertise a mode without a matching handler. If the server sends that mode, the connection returns an error because it cannot handle the request.

Form mode

Form mode collects structured, non-sensitive data in the client. The request includes a restricted JSON Schema in requestedSchema. If the user submits the form, return action: "accept" with matching content:

this.mcp.configureElicitationHandlers({
    form: async (request) => {
        const content = await showFormToUser(request.params.requestedSchema);
        return content ? { action: "accept", content } : { action: "cancel" };
    },
});
this.mcp.configureElicitationHandlers({
    form: async (request) => {
        const content = await showFormToUser(request.params.requestedSchema);
        return content ? { action: "accept", content } : { action: "cancel" };
    },
});

Allow the user to review and edit values before submission. Validate accepted content against requestedSchema. Do not use form mode to request passwords, API keys, access tokens, payment credentials, or other secrets.

URL mode

URL mode asks the user to open an external page. Use it for out-of-band interactions that may collect secrets, such as third-party authorization or payment. Keep the URL in the dedicated elicitation path and out of model-visible messages and tool-result text.

A URL handler should:

  1. Show which MCP server sent the request.
  2. Show the request message, target host, and full URL.
  3. Ask for consent before opening the URL.
  4. Open the page in a browser context the Agent and model cannot inspect.
  5. Return action: "accept" without content after consent.
  6. Offer distinct decline and cancel controls.

Do not prefetch the URL or its metadata. Treat the URL as untrusted input. Production servers should send HTTPS URLs.

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

Response actions

Both modes support 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.

Include content only for an accepted form response. Omit it for URL, decline, and cancel responses.

Forward elicitation to a UI

A handler returns a promise, but the response often comes from a browser. Broadcast the request to connected clients, then resolve the promise through a @callable method:

import { Agent, callable } from "agents";

class MyAgent extends Agent {
    pendingElicitations = new Map();

onStart() {
        this.mcp.configureElicitationHandlers({
            form: (request, serverId) => this.forward(request, serverId),
            url: (request, serverId) => this.forward(request, serverId),
        });
    }

forward(request, serverId) {
        const id = crypto.randomUUID();

const result = new Promise((resolve) => {
            const timeout = setTimeout(() => {
                if (this.pendingElicitations.delete(id)) {
                    resolve({ action: "cancel" });
                }
            }, 55_000);
            this.pendingElicitations.set(id, { resolve, timeout });
        });

this.broadcast(
            JSON.stringify({
                type: "mcp-elicitation",
                id,
                serverId,
                params: request.params,
            }),
        );

return result;
    }

@callable()
    respondToElicitation(id, result) {
        const pending = this.pendingElicitations.get(id);
        if (!pending) return;

this.pendingElicitations.delete(id);
        clearTimeout(pending.timeout);
        pending.resolve(result);
    }
}
import { Agent, callable } from "agents";
import type { ElicitRequest, ElicitResult } from "agents/mcp/client";

type PendingResolver = {
    resolve: (result: ElicitResult) => void;
    timeout: ReturnType<typeof setTimeout>;
};

class MyAgent extends Agent<Env> {
    private pendingElicitations = new Map<string, PendingResolver>();

onStart() {
        this.mcp.configureElicitationHandlers({
            form: (request, serverId) => this.forward(request, serverId),
            url: (request, serverId) => this.forward(request, serverId),
        });
    }

private forward(
        request: ElicitRequest,
        serverId: string,
    ): Promise<ElicitResult> {
        const id = crypto.randomUUID();

const result = new Promise<ElicitResult>((resolve) => {
            const timeout = setTimeout(() => {
                if (this.pendingElicitations.delete(id)) {
                    resolve({ action: "cancel" });
                }
            }, 55_000);
            this.pendingElicitations.set(id, { resolve, timeout });
        });

this.broadcast(
            JSON.stringify({
                type: "mcp-elicitation",
                id,
                serverId,
                params: request.params,
            }),
        );

return result;
    }

@callable()
    respondToElicitation(id: string, result: ElicitResult) {
        const pending = this.pendingElicitations.get(id);
        if (!pending) return;

this.pendingElicitations.delete(id);
        clearTimeout(pending.timeout);
        pending.resolve(result);
    }
}

The example uses a 55-second timeout because MCP SDK requests default to 60 seconds. If your client call sets a longer request timeout, adjust this timeout to finish first.

Refer to the mcp-client example ↗︎ for the browser implementation. mcp-elicitation-mrtr ↗︎ demonstrates stateless elicitation. mcp-elicitation ↗︎ demonstrates legacy elicitation.

For server-side patterns, refer to Elicitation with a stateless handler and Elicitation on legacy servers.

Managing servers

MCP server registrations persist across Agent restarts. The SDK stores server configuration in SQLite, stores OAuth tokens securely, and restores connections when the Agent wakes.

List all servers

const state = this.getMcpServers();

for (const [id, server] of Object.entries(state.servers)) {
    console.log(`${id}: ${server.name} (${server.server_url})`);
}
const state = this.getMcpServers();

for (const [id, server] of Object.entries(state.servers)) {
    console.log(`${id}: ${server.name} (${server.server_url})`);
}

Get server status

Use the server ID to inspect an individual connection:

const state = this.getMcpServers();
const server = state.servers[serverId];

if (server) {
    console.log(`${server.name}: ${server.state}`);
    // state: "ready" | "authenticating" | "connecting" | "connected" | "discovering" | "failed"
}
const state = this.getMcpServers();
const server = state.servers[serverId];

if (server) {
    console.log(`${server.name}: ${server.state}`);
    // state: "ready" | "authenticating" | "connecting" | "connected" | "discovering" | "failed"
}

Remove a server

await this.removeMcpServer(serverId);
await this.removeMcpServer(serverId);

This disconnects from the server and removes it from storage.

Client-side integration

Connected clients receive real-time MCP updates via WebSocket:

import { useAgent } from "agents/react";
import { useState } from "react";

function Dashboard() {
    const [tools, setTools] = useState([]);
    const [servers, setServers] = useState({});

const agent = useAgent({
        agent: "MyAgent",
        onMcpUpdate: (mcpState) => {
            setTools(mcpState.tools);
            setServers(mcpState.servers);
        },
    });

return (
        <div>
            <h2>Connected Servers</h2>
            {Object.entries(servers).map(([id, server]) => (
                <div key={id}>
                    {server.name}: {server.state}
                </div>
            ))}

<h2>Available Tools ({tools.length})</h2>
            {tools.map((tool) => (
                <div key={`${tool.serverId}-${tool.name}`}>{tool.name}</div>
            ))}
        </div>
    );
}