Claude Tools
Use Case: Define MCP tools that Claude Desktop and Claude Code can discover and invoke. Each tool is a typed function with a schema that Claude uses to decide when and how to call it.
Prerequisites
- Claude Desktop or Claude Code with MCP client support
- An MCP server running locally or deployed (Node.js +
@modelcontextprotocol/sdk) - Your server's URL or
stdiotransport configured in Claude'sclaude_desktop_config.json
Setup & Configuration
Claude's MCP server config points to your server. Use stdio for local tools (no network overhead) or SSE for remote servers:
claude_desktop_config.jsonjson
{
"mcpServers": {
"mcp-dustin": {
"command": "node",
"args": ["dist/mcp/server.js"],
"env": {
"TURSO_DATABASE_URL": "libsql://my-db.turso.io",
"TURSO_AUTH_TOKEN": "eyJ..."
}
},
"remote-tools": {
"transport": "sse",
"url": "https://my-site.vercel.app/mcp"
}
}
}Define tools using the MCP SDK. Each tool has a name, description, and input schema that Claude introspects:
src/mcp/server.tstypescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{ name: "mcp-dustin", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
...bookmarkTools,
...databaseTools,
...searchTools,
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// Route to the appropriate tool handler
return toolHandlers[name](args ?? {});
});
const transport = new StdioServerTransport();
await server.connect(transport);Core Implementation
Bookmark management tool (based on the real Bookmark-Manager extension pattern):
src/mcp/tools/bookmarks.tstypescript
import { z } from "zod";
import { getDb } from "../lib/db";
const BookmarkInput = z.object({
title: z.string().min(1).max(200),
url: z.string().url(),
tags: z.array(z.string()).max(10).optional(),
});
export const bookmarkToolDef = {
name: "bookmark_save",
description: "Save a URL as a bookmark with optional tags",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Page title" },
url: { type: "string", format: "uri", description: "Page URL" },
tags: {
type: "array",
items: { type: "string" },
description: "Optional tags for organization",
},
},
required: ["title", "url"],
},
};
export async function handleBookmarkSave(raw: Record<string, unknown>) {
const parsed = BookmarkInput.parse(raw);
const db = getDb();
const id = crypto.randomUUID();
await db.execute({
sql: "INSERT INTO bookmarks (id, title, url, tags) VALUES (?, ?, ?, ?)",
args: [id, parsed.title, parsed.url, JSON.stringify(parsed.tags ?? [])],
});
return {
content: [{ type: "text", text: `Saved bookmark: ${parsed.title}` }],
};
}
export const bookmarkListToolDef = {
name: "bookmark_list",
description: "List saved bookmarks, optionally filtered by tag",
inputSchema: {
type: "object",
properties: {
tag: { type: "string", description: "Filter by tag" },
},
},
};
export async function handleBookmarkList(raw: Record<string, unknown>) {
const db = getDb();
const result = raw.tag
? await db.execute({
sql: "SELECT * FROM bookmarks WHERE tags LIKE ? ORDER BY created_at DESC",
args: [`%"${raw.tag}"%`],
})
: await db.execute("SELECT * FROM bookmarks ORDER BY created_at DESC LIMIT 50");
return {
content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }],
};
}Web search and scrape tool for Claude to fetch live information:
src/mcp/tools/search.tstypescript
export const searchToolDef = {
name: "web_search",
description: "Search the web for current information on a topic",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
maxResults: { type: "number", default: 5 },
},
required: ["query"],
},
};
export async function handleWebSearch(raw: Record<string, unknown>) {
const query = String(raw.query ?? "");
const maxResults = Number(raw.maxResults ?? 5);
// Use a search API (DuckDuckGo, SerpAPI, etc.)
const res = await fetch(
`https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json`
);
const data = await res.json();
const results = (data.RelatedTopics ?? [])
.slice(0, maxResults)
.map((r: any) => ({
title: r.Text?.split(" - ")[0] ?? "Result",
snippet: r.Text ?? "",
url: r.FirstURL ?? "",
}));
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}Deployment Notes
- Tool discovery: Claude calls
tools/liston connect. Keep descriptions concise but specific - Claude uses them to pick the right tool. - Rate limits: Claude Desktop sends tool calls sequentially. For concurrent requests, use SSE transport with a server that handles parallel invocations.
- Error messages: Return structured errors (
{ isError: true, content: "..." }) so Claude can retry or apologize. - Security: Validate all inputs server-side. Claude's schema is a hint, not a guarantee - a compromised client could send bad data.
- State: MCP tools are stateless by design. Use the database-queries pattern for persistence.
- Timeouts: Claude expects responses within 5 minutes for tool calls. For long operations, use an async pattern with a status-check tool.