branch:
ServerDemo.tsx
9855 bytesRaw
import { useState } from "react";
import { Button, Input, InputArea, Surface, Text } from "@cloudflare/kumo";
import { DemoWrapper } from "../../layout";
import {
LogPanel,
CodeExplanation,
HighlightedJson,
type CodeSection
} from "../../components";
import { useLogs, useToast } from "../../hooks";
const codeSections: CodeSection[] = [
{
title: "Create an MCP server agent",
description:
'Extend McpAgent instead of Agent. Create an McpServer instance and register tools, resources, and prompts in the init() method. Deploy with McpAgent.serve("/mcp") to expose the MCP protocol endpoint.',
code: `import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
class PlaygroundMcpServer extends McpAgent<Env> {
server = new McpServer({ name: "playground", version: "1.0.0" });
async init() {
this.server.registerTool(
"roll_dice",
{
description: "Roll dice with N sides",
inputSchema: { sides: z.number().default(6) },
},
async ({ sides }) => ({
content: [{ type: "text", text: String(Math.floor(Math.random() * sides) + 1) }],
})
);
this.server.resource("info", "playground://info", async (uri) => ({
contents: [{ uri: uri.href, text: "Server info..." }],
}));
}
}
// Expose the MCP server at /mcp
export default PlaygroundMcpServer.serve("/mcp", {
binding: "PlaygroundMcpServer",
});`
},
{
title: "Connect from any MCP client",
description:
"Once deployed, any MCP-compatible client (Claude, Cursor, custom apps) can connect to your agent's URL and use its tools and resources.",
code: `// In Claude Desktop or Cursor settings:
{
"mcpServers": {
"playground": {
"url": "https://your-app.workers.dev/mcp"
}
}
}`
}
];
const TOOLS = [
{
name: "roll_dice",
description: "Roll one or more dice with a given number of sides",
defaultArgs: { sides: 6, count: 2 }
},
{
name: "generate_uuid",
description: "Generate one or more random UUIDs",
defaultArgs: { count: 3 }
},
{
name: "word_count",
description: "Count words, characters, and lines in text",
defaultArgs: { text: "The quick brown fox jumps over the lazy dog" }
},
{
name: "hash_text",
description: "Compute SHA-256 hash of text",
defaultArgs: { text: "hello world" }
}
];
const MCP_HEADERS = {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream"
};
async function mcpRequest(
url: string,
method: string,
params: Record<string, unknown>,
sessionId?: string
): Promise<{ data: unknown; sessionId: string | null }> {
const headers: Record<string, string> = { ...MCP_HEADERS };
if (sessionId) {
headers["Mcp-Session-Id"] = sessionId;
}
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify({
jsonrpc: "2.0",
id: crypto.randomUUID(),
method,
params
})
});
const newSessionId = response.headers.get("Mcp-Session-Id");
const contentType = response.headers.get("Content-Type") ?? "";
if (contentType.includes("text/event-stream")) {
const text = await response.text();
const lines = text.split("\n");
let lastData: unknown = null;
for (const line of lines) {
if (line.startsWith("data: ")) {
try {
lastData = JSON.parse(line.slice(6));
} catch {
// skip non-JSON data lines
}
}
}
return { data: lastData, sessionId: newSessionId };
}
const data = await response.json();
return { data, sessionId: newSessionId };
}
async function ensureSession(
url: string,
currentSessionId: string | null
): Promise<string> {
if (currentSessionId) return currentSessionId;
const { sessionId } = await mcpRequest(url, "initialize", {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "playground", version: "1.0.0" }
});
if (sessionId) {
await mcpRequest(url, "notifications/initialized", {}, sessionId);
}
return sessionId ?? "";
}
export function McpServerDemo() {
const { logs, addLog, clearLogs } = useLogs();
const { toast } = useToast();
const [selectedTool, setSelectedTool] = useState(0);
const [argsText, setArgsText] = useState(
JSON.stringify(TOOLS[0].defaultArgs, null, 2)
);
const [result, setResult] = useState<unknown>(null);
const [isRunning, setIsRunning] = useState(false);
const [sessionId, setSessionId] = useState<string | null>(null);
const mcpUrl = `${window.location.origin}/mcp-server`;
const handleSelectTool = (index: number) => {
setSelectedTool(index);
setArgsText(JSON.stringify(TOOLS[index].defaultArgs, null, 2));
setResult(null);
};
const handleCallTool = async () => {
const tool = TOOLS[selectedTool];
let args: Record<string, unknown>;
try {
args = JSON.parse(argsText);
} catch {
addLog("error", "error", "Invalid JSON in arguments");
return;
}
setIsRunning(true);
setResult(null);
try {
const sid = await ensureSession(mcpUrl, sessionId);
if (!sessionId && sid) {
setSessionId(sid);
addLog("info", "session", { id: sid });
}
addLog("out", "call_tool", { name: tool.name, args });
const { data } = await mcpRequest(
mcpUrl,
"tools/call",
{ name: tool.name, arguments: args },
sid
);
addLog("in", "result", data);
setResult(data);
toast(tool.name + " called", "success");
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
toast(e instanceof Error ? e.message : String(e), "error");
setSessionId(null);
} finally {
setIsRunning(false);
}
};
return (
<DemoWrapper
title="MCP Server"
description={
<>
This playground exposes a real MCP server at{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
/mcp-server
</code>
. It registers tools (roll_dice, generate_uuid, word_count, hash_text)
and a resource. Any MCP-compatible client — Claude, Cursor, or the MCP
Client demo on the next page — can connect and use them. Test the
tools below.
</>
}
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Controls */}
<div className="space-y-6">
{/* Server URL */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-2">
<Text variant="heading3">MCP Server URL</Text>
</div>
<div className="flex gap-2">
<Input
aria-label="MCP server URL"
type="text"
value={mcpUrl}
readOnly
className="flex-1 font-mono text-xs"
/>
<Button
variant="secondary"
onClick={() => navigator.clipboard.writeText(mcpUrl)}
>
Copy
</Button>
</div>
<p className="text-xs text-kumo-subtle mt-2">
Add this URL to Claude Desktop, Cursor, or any MCP client
</p>
</Surface>
{/* Available Tools */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">Available Tools</Text>
</div>
<div className="space-y-2">
{TOOLS.map((tool, i) => (
<button
key={tool.name}
type="button"
onClick={() => handleSelectTool(i)}
className={`w-full text-left p-3 rounded border transition-colors ${
selectedTool === i
? "border-kumo-brand bg-kumo-elevated"
: "border-kumo-line hover:border-kumo-interact"
}`}
>
<code className="text-sm font-semibold text-kumo-default">
{tool.name}
</code>
<div className="mt-1">
<Text variant="secondary" size="xs">
{tool.description}
</Text>
</div>
</button>
))}
</div>
</Surface>
{/* Test Tool */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">Test: {TOOLS[selectedTool].name}</Text>
</div>
<InputArea
aria-label="Tool arguments (JSON)"
value={argsText}
onChange={(e) => setArgsText(e.target.value)}
className="w-full h-24 font-mono text-sm mb-3"
/>
<Button
variant="primary"
onClick={handleCallTool}
disabled={isRunning}
className="w-full"
>
{isRunning ? "Calling..." : `Call ${TOOLS[selectedTool].name}`}
</Button>
</Surface>
{/* Result */}
{result !== null && (
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-2">
<Text variant="heading3">Result</Text>
</div>
<HighlightedJson data={result} />
</Surface>
)}
</div>
{/* Logs */}
<div className="space-y-6">
<LogPanel logs={logs} onClear={clearLogs} maxHeight="600px" />
</div>
</div>
<CodeExplanation sections={codeSections} />
</DemoWrapper>
);
}