agent-frameworks/claude-tools

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 stdio transport configured in Claude's claude_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/list on 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.