Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/shaggy-rice-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"mcp-elicitation-example": patch
"agents": patch
---

add elicitation support and examples
2 changes: 1 addition & 1 deletion examples/mcp-client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"author": "",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.13.3",
"@modelcontextprotocol/sdk": "1.13.3",
"nanoid": "^5.1.5"
},
"keywords": [],
Expand Down
293 changes: 293 additions & 0 deletions examples/mcp-elicitation-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
# MCP Elicitation Demo

This is a MCP client-server example that shows how to use elicitation support using the Agents SDK.

- **Full MCP compliance** with https://modelcontextprotocol.io/specification/draft/client/elicitation

### MCP Server (`McpServerAgent`)

Here's the actual working code from our demo:

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpAgent, type ElicitResult } from "agents/mcp";
import {
Agent,
type AgentNamespace,
routeAgentRequest,
unstable_callable as callable,
type Connection,
type WSMessage
} from "agents";
import { z } from "zod";

type Env = {
MyAgent: AgentNamespace<MyAgent>;
McpServerAgent: DurableObjectNamespace<McpServerAgent>;
HOST: string;
};

export class McpServerAgent extends McpAgent<Env, { counter: number }, {}> {
server = new McpServer({
name: "Elicitation Demo Server",
version: "1.0.0"
})

initialState = { counter: 0 };

// Track active session for cross-agent elicitation
private activeSession: string | null = null;

async elicitInput(params: {
message: string;
requestedSchema: {
type: string;
properties?: Record<
string,
{
type: string;
title?: string;
description?: string;
format?: string;
enum?: string[];
enumNames?: string[];
}
>;
required?: string[];
};
}): Promise<ElicitResult> {
if (!this.activeSession) {
throw new Error("No active client session found for elicitation");
}

// Get the MyAgent instance that handles browser communication
const myAgentId = this.env.MyAgent.idFromName(this.activeSession);
const myAgent = this.env.MyAgent.get(myAgentId);

// Create MCP-compliant elicitation request
const requestId = `elicit_${Math.random().toString(36).substring(2, 11)}`;
const elicitRequest = {
jsonrpc: "2.0" as const,
id: requestId,
method: "elicitation/create",
params: {
message: params.message,
requestedSchema: params.requestedSchema
}
};

// Forward request to MyAgent which communicates with browser
const response = await myAgent.fetch(
new Request("https://internal/elicit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(elicitRequest)
})
);

if (!response.ok) {
throw new Error("Failed to send elicitation request");
}

return (await response.json()) as ElicitResult;
}

async init() {
// Counter tool with user confirmation via elicitation
this.server.tool(
"increment-counter",
"Increment the counter with user confirmation",
{
amount: z.number().describe("Amount to increment by").default(1),
__clientSession: z
.string()
.optional()
.describe("Internal client session ID")
},
async ({
amount,
__clientSession
}: {
amount: number;
__clientSession?: string;
}) => {
// Store session for cross-agent elicitation
if (__clientSession) {
this.activeSession = __clientSession;
}

// Request user confirmation via elicitation
const confirmation = await this.elicitInput({
message: `Are you sure you want to increment the counter by ${amount}?`,
requestedSchema: {
type: "object",
properties: {
confirmed: {
type: "boolean",
title: "Confirm increment",
description: "Check to confirm the increment"
}
},
required: ["confirmed"]
}
});

if (
confirmation.action === "accept" &&
confirmation.content?.confirmed
) {
this.setState({
counter: this.state.counter + amount
});

return {
content: [
{
type: "text",
text: `Counter incremented by ${amount}. New value: ${this.state.counter}`
}
]
};
} else {
return {
content: [
{
type: "text",
text: "Counter increment cancelled."
}
]
};
}
}
);

// User creation tool with form-based elicitation
this.server.tool(
"create-user",
"Create a new user with form input",
{
username: z.string().describe("Username for the new user"),
__clientSession: z
.string()
.optional()
.describe("Internal client session ID")
},
async ({
username,
__clientSession
}: {
username: string;
__clientSession?: string;
}) => {
// Store session for cross-agent elicitation
if (__clientSession) {
this.activeSession = __clientSession;
}

// Request user details via elicitation
const userInfo = await this.elicitInput({
message: `Create user account for "${username}":`,
requestedSchema: {
type: "object",
properties: {
email: {
type: "string",
format: "email",
title: "Email Address",
description: "User's email address"
},
role: {
type: "string",
title: "Role",
enum: ["viewer", "editor", "admin"],
enumNames: ["Viewer", "Editor", "Admin"]
},
sendWelcome: {
type: "boolean",
title: "Send Welcome Email",
description: "Send welcome email to user"
}
},
required: ["email", "role"]
}
});

if (userInfo.action === "accept" && userInfo.content) {
const details = userInfo.content;
return {
content: [
{
type: "text",
text: `User created:\n• Username: ${username}\n• Email: ${details.email}\n• Role: ${details.role}\n• Welcome email: ${details.sendWelcome ? "Yes" : "No"}`
}
]
};
} else {
return {
content: [
{
type: "text",
text: "User creation cancelled."
}
]
};
}
}
);
}
```

## Getting Started

1. Install dependencies:

```bash
npm install
```

2. Start the development server:

```bash
npm start
```

3. Open your browser (typically http://localhost:5173/)

4. The demo auto-connects to the local MCP server

5. Try the elicitation tools:
- **Increment Counter**: Click to see boolean confirmation elicitation
- **Create User**: Click to see complex form elicitation

### In production, your MCP server typically connects directly to clients, making elicitation much simpler:

```typescript
// Direct MCP connection
export class MyMcpServer extends McpAgent {
async init() {
this.server.tool(
"my-tool",
"My tool",
{ input: z.string() },
async ({ input }) => {
// elicitInput() works directly
const result = await this.elicitInput({
message: "Confirm this action?",
requestedSchema: {
type: "object",
properties: {
confirmed: { type: "boolean", title: "Confirm" }
},
required: ["confirmed"]
}
});

if (result.action === "accept" && result.content?.confirmed) {
return { content: [{ type: "text", text: "Action completed!" }] };
}
return { content: [{ type: "text", text: "Action cancelled." }] };
}
);
}
}
```
13 changes: 13 additions & 0 deletions examples/mcp-elicitation-demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MCP Elicitation Demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/client.tsx"></script>
</body>
</html>
19 changes: 19 additions & 0 deletions examples/mcp-elicitation-demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"author": "",
"dependencies": {
"@modelcontextprotocol/sdk": "1.13.3",
"nanoid": "^5.1.5",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"zod": "^3.25.67"
},
"keywords": [],
"name": "@cloudflare/agents-mcp-elicitation-demo",
"private": true,
"scripts": {
"build": "vite build",
"deploy": "wrangler deploy",
"start": "vite dev"
},
"type": "module"
}
Binary file added examples/mcp-elicitation-demo/public/favicon.ico
Binary file not shown.
Loading