MCP Tools Server
Remote MCP server on Workers with Agents SDK McpAgent - expose DNS, headers, uptime, and hash tools to Cursor and Claude
A remote Model Context Protocol (MCP) server built with the Agents SDK McpAgent. Each MCP session runs in its own SQLite-backed Durable Object, and clients such as Cursor, Claude Desktop, or the MCP Inspector connect over Streamable HTTP at /mcp.
Features
dns_lookup- ResolveA,AAAA,MX,TXT,NS, orCNAMErecords through Cloudflare DNS-over-HTTPS (1.1.1.1)http_headers- Fetch a URL from the edge and return status, final URL, and response headersis_it_up- Reachability check with latency in millisecondshash_text- SHA-256 / SHA-384 / SHA-512 digests via Web Crypto- Ready-made client config -
GET /returns your endpoint URL and a paste-readymcp.jsonsnippet
API Reference
GET /
Returns app info, the MCP endpoint for the current host, the tool list, and an mcp.json snippet.
Example Request
curl "https://mcp-tools-server.your-subdomain.workers.dev/"Success Response
{
"name": "mcp-tools-server",
"description": "Remote MCP server exposing DNS, HTTP, uptime, and hashing tools",
"usage": {
"mcp": "POST /mcp (Streamable HTTP)",
"inspector": "npx @modelcontextprotocol/inspector, then connect to https://mcp-tools-server.your-subdomain.workers.dev/mcp"
},
"mcpEndpoint": "https://mcp-tools-server.your-subdomain.workers.dev/mcp",
"tools": ["dns_lookup", "http_headers", "is_it_up", "hash_text"],
"mcpConfig": {
"mcpServers": {
"mcp-tools-server": {
"command": "npx",
"args": ["mcp-remote", "https://mcp-tools-server.your-subdomain.workers.dev/mcp"]
}
}
},
"cloudflareFeatures": ["Agents SDK (McpAgent)", "Durable Objects"]
}Error Codes
404- Unknown path (NOT_FOUND)
POST /mcp
The MCP Streamable HTTP endpoint, served by ToolsMcp.serve("/mcp", { binding: "MCP_OBJECT" }). Send JSON-RPC messages with an MCP client rather than by hand. The first initialize request creates a session, and the mcp-session-id response header routes later requests to the same Durable Object.
Tools
Each tool returns one text content block containing JSON. On failure the result has isError: true and the text is { "error": string, "code": string }.
dns_lookup
hostname string (required)
Hostname such as cloudflare.com. An http(s) URL is also accepted, and its hostname is used.
type "A" | "AAAA" | "MX" | "TXT" | "NS" | "CNAME" (optional, default A)
{
"name": "cloudflare.com",
"type": "A",
"status": 0,
"answers": [{ "name": "cloudflare.com", "type": 1, "ttl": 300, "data": "104.16.132.229" }]
}Errors: INVALID_HOSTNAME, DNS_ERROR.
http_headers
url string (required) - http or https only.
{
"url": "https://www.cloudflare.com/",
"finalUrl": "https://www.cloudflare.com/",
"status": 200,
"statusText": "OK",
"headers": { "content-type": "text/html; charset=utf-8", "server": "cloudflare" }
}Errors: INVALID_URL, FETCH_ERROR.
is_it_up
url string (required) - http or https only.
{ "url": "https://www.cloudflare.com/", "up": true, "status": 200, "latencyMs": 84 }When the request fails, up is false, status is null, and an error message is included. Errors: INVALID_URL.
hash_text
text string (required) - Up to 100,000 characters.
algorithm "SHA-256" | "SHA-384" | "SHA-512" (optional, default SHA-256)
{
"algorithm": "SHA-256",
"hex": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}Connecting clients
Cursor and Claude Desktop
Both can use mcp-remote to bridge a local stdio client to a remote server. Add this to ~/.cursor/mcp.json (Cursor) or claude_desktop_config.json (Claude Desktop), then restart the client:
{
"mcpServers": {
"mcp-tools-server": {
"command": "npx",
"args": ["mcp-remote", "https://mcp-tools-server.your-subdomain.workers.dev/mcp"]
}
}
}You can copy the same snippet, filled in with your hostname, from GET /. Clients that support remote Streamable HTTP servers natively can use the /mcp URL directly.
MCP Inspector
npx @modelcontextprotocol/inspectorIn the Inspector UI, choose Streamable HTTP as the transport, enter https://<your-worker>/mcp (or http://localhost:8787/mcp locally), click Connect, and then open Tools → List Tools to call each tool interactively.
Implementation Details
src/mcp-agent.ts-ToolsMcp extends McpAgentcreates an SDKMcpServerand callsregisterTools()ininit()src/lib/tools.ts- Registers the four tools with Zod input schemas and read-only annotationssrc/lib/dns.ts,src/lib/http.ts,src/lib/hash.ts- Plain functions with no MCP dependency, unit-tested with a mockedfetchsrc/index.ts- Sends/mcpto theMcpAgenthandler and everything else to Hono
Cloudflare now recommends the stateless createMcpHandler (MCP SDK v2) for new servers, and
McpAgent is feature-frozen. This experiment uses McpAgent to show the Durable Object–backed
session model. Because the tools live in src/lib/tools.ts, you can move them to a stateless
handler later.
Use Cases
- Give Cursor or Claude network-debugging tools that run from Cloudflare's edge rather than your laptop
- Learn the
McpAgent+ Durable Object session model before building stateful MCP tools - Use as a starting point for an internal MCP server (add your own tools in
src/lib/tools.ts) - Test MCP clients and the Inspector against a real remote server
Limitations
- No authentication: anyone with the URL can call the tools. Add OAuth (Workers OAuth Provider) or put the Worker behind Cloudflare Access before exposing internal tools
http_headersandis_it_upfollow redirects and time out after 10 seconds- Each MCP session creates a Durable Object; idle sessions hibernate but still count toward storage
McpAgentis deprecated in favor of statelesscreateMcpHandler; see the callout above
Use in your project
Copy these files into an existing Worker. Prefer Deployment to try the full experiment first. Source: apps/experiments/mcp-tools-server.
npm install agents @modelcontextprotocol/sdk zodimport type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";import { DNS_RECORD_TYPES, HASH_ALGORITHMS, MAX_HASH_INPUT_LENGTH } from "../constants/defaults";import { toolError, toolResult } from "../utils/mcp";import { dnsLookup, normalizeHostname } from "./dns";import { hashText } from "./hash";import { checkUptime, getHttpHeaders } from "./http";import { validateUrl } from "./url";const READ_ONLY = { readOnlyHint: true, openWorldHint: true } as const;/** Registers every tool on an MCP server. Works with any SDK v1 `McpServer`. */export function registerTools(server: McpServer): void { server.registerTool( "dns_lookup", { title: "DNS lookup", description: "Resolve DNS records for a hostname via Cloudflare DNS-over-HTTPS (1.1.1.1).", inputSchema: { hostname: z.string().describe("Hostname or URL, e.g. cloudflare.com"), type: z.enum(DNS_RECORD_TYPES).default("A").describe("Record type"), }, annotations: READ_ONLY, }, async ({ hostname, type }) => { const host = normalizeHostname(hostname); if (!host) return toolError("Invalid hostname", "INVALID_HOSTNAME"); try { return toolResult(await dnsLookup(host, type)); } catch (e) { return toolError(e instanceof Error ? e.message : "DNS lookup failed", "DNS_ERROR"); } } ); server.registerTool( "http_headers", { title: "HTTP headers", description: "Fetch a URL from Cloudflare's edge and return the status code and response headers.", inputSchema: { url: z.string().describe("http(s) URL to fetch") }, annotations: READ_ONLY, }, async ({ url }) => { const target = validateUrl(url); if (!target) return toolError("Invalid URL (http/https only)", "INVALID_URL"); try { return toolResult(await getHttpHeaders(target)); } catch (e) { return toolError(e instanceof Error ? e.message : "Fetch failed", "FETCH_ERROR"); } } ); server.registerTool( "is_it_up", { title: "Is it up?", description: "Check whether a URL is reachable from Cloudflare's edge and measure latency.", inputSchema: { url: z.string().describe("http(s) URL to check") }, annotations: READ_ONLY, }, async ({ url }) => { const target = validateUrl(url); if (!target) return toolError("Invalid URL (http/https only)", "INVALID_URL"); return toolResult(await checkUptime(target)); } ); server.registerTool( "hash_text", { title: "Hash text", description: "Hash UTF-8 text with SHA-256, SHA-384, or SHA-512 using Web Crypto.", inputSchema: { text: z.string().max(MAX_HASH_INPUT_LENGTH).describe("Text to hash"), algorithm: z.enum(HASH_ALGORITHMS).default("SHA-256").describe("Hash algorithm"), }, annotations: { readOnlyHint: true, openWorldHint: false }, }, async ({ text, algorithm }) => toolResult(await hashText(text, algorithm)) );}Deployment
Deploy
wrangler.json already declares the MCP_OBJECT Durable Object binding, the new_sqlite_classes migration for ToolsMcp, and the nodejs_compat flag.
Test your deployment
curl "https://mcp-tools-server.your-subdomain.workers.dev/"
npx @modelcontextprotocol/inspectorLocal Development
cd apps/experiments/mcp-tools-server
npm install
npm run devcurl "http://localhost:8787/"
# MCP endpoint for clients and the Inspector: http://localhost:8787/mcpConfiguration
wrangler.json declares:
- Durable Object binding
MCP_OBJECT→ classToolsMcp(the default binding nameMcpAgent.serve()looks for) - Migration
v1withnew_sqlite_classes: ["ToolsMcp"] - Compatibility flag
nodejs_compat(required by the Agents SDK)
Cloudflare Features Used
- Agents SDK - MCP -
McpAgentand Streamable HTTP transport - Durable Objects - One SQLite-backed instance per MCP session
- DNS over HTTPS - JSON API used by
dns_lookup - Web Crypto -
crypto.subtle.digestforhash_text