Claude-initial-setup mcp-tool-design

install
source · Clone the upstream repo
git clone https://github.com/VersoXBT/claude-initial-setup
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/VersoXBT/claude-initial-setup "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/mcp-development/mcp-tool-design" ~/.claude/skills/versoxbt-claude-initial-setup-mcp-tool-design && rm -rf "$T"
manifest: skills/mcp-development/mcp-tool-design/SKILL.md
source content

MCP Tool Design

Best practices for designing well-structured, reliable MCP tools. Covers naming, schemas, error handling, pagination, idempotency, and effective descriptions.

When to Use

  • User is designing new tools for an MCP server
  • User is writing JSON Schema for tool inputs
  • User needs pagination or idempotency patterns
  • User wants guidance on tool naming or descriptions
  • User is handling errors in MCP tool responses

Core Patterns

Naming Conventions

Use snake_case, verb-first names. Group related tools with a common prefix.

// Good: verb_noun pattern, clear scope
server.tool("search_issues", ...);
server.tool("create_issue", ...);
server.tool("update_issue", ...);
server.tool("list_repositories", ...);
server.tool("get_file_contents", ...);

// Bad: inconsistent, vague, or noun-first
server.tool("issues", ...);          // What action?
server.tool("doSearch", ...);        // camelCase
server.tool("repository-list", ...); // kebab-case, noun-first

Writing Effective Descriptions

Descriptions tell Claude WHEN and WHY to use the tool. Be specific and directive.

// Good: explains purpose, trigger conditions, and constraints
server.tool(
  "search_codebase",
  "Search for code patterns across the repository using regex. " +
  "Use this when the user asks to find functions, classes, imports, " +
  "or specific code patterns. Supports file type filtering. " +
  "Returns up to 50 matches per call -- use pagination for more.",
  schema,
  handler
);

// Bad: vague, tells Claude nothing useful
server.tool(
  "search",
  "Searches for things.",
  schema,
  handler
);

Input Schema Design

Use JSON Schema with descriptive property names, descriptions, and constraints.

import { z } from "zod";

server.tool(
  "search_issues",
  "Search GitHub issues by query, state, and labels.",
  {
    query: z.string()
      .describe("Search query string. Searches title and body text."),
    state: z.enum(["open", "closed", "all"])
      .default("open")
      .describe("Filter by issue state. Default: open."),
    labels: z.array(z.string())
      .optional()
      .describe("Filter by label names. Returns issues matching ALL labels."),
    limit: z.number()
      .int()
      .min(1)
      .max(100)
      .default(20)
      .describe("Maximum results to return. Range: 1-100. Default: 20."),
    cursor: z.string()
      .optional()
      .describe("Pagination cursor from a previous response.")
  },
  handler
);

Error Response Patterns

Return structured errors with the

isError
flag so Claude can reason and recover.

server.tool("get_file", "Read a file by path.", { path: z.string() }, async ({ path }) => {
  // Validate input
  if (path.includes("..")) {
    return {
      isError: true,
      content: [{ type: "text", text: "Path traversal not allowed. Use absolute paths within the project." }]
    };
  }

  try {
    const content = await fs.readFile(path, "utf-8");
    return {
      content: [{ type: "text", text: content }]
    };
  } catch (error) {
    if (error.code === "ENOENT") {
      return {
        isError: true,
        content: [{ type: "text", text: `File not found: ${path}. Check the path and try again.` }]
      };
    }
    return {
      isError: true,
      content: [{ type: "text", text: `Failed to read file: ${error.message}` }]
    };
  }
});

Pagination Pattern

Use cursor-based pagination for tools that return large result sets.

server.tool(
  "list_commits",
  "List commits for a repository branch. Returns 20 commits per page with a cursor for pagination.",
  {
    repo: z.string().describe("Repository name in owner/repo format."),
    branch: z.string().default("main").describe("Branch name. Default: main."),
    cursor: z.string().optional().describe("Pagination cursor from previous response.")
  },
  async ({ repo, branch, cursor }) => {
    const page = cursor ? decodeCursor(cursor) : 1;
    const commits = await fetchCommits(repo, branch, page, 20);
    const hasMore = commits.length === 20;

    const result = {
      commits: commits.map(c => ({
        sha: c.sha,
        message: c.message,
        author: c.author,
        date: c.date
      })),
      next_cursor: hasMore ? encodeCursor(page + 1) : null,
      has_more: hasMore
    };

    return {
      content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
    };
  }
);

Idempotency Pattern

Design tools so repeated calls with the same input produce the same result.

server.tool(
  "set_config",
  "Set a configuration value. Idempotent -- calling with the same key and value multiple times has no additional effect.",
  {
    key: z.string().describe("Configuration key."),
    value: z.string().describe("Configuration value.")
  },
  async ({ key, value }) => {
    const current = await getConfig(key);
    if (current === value) {
      return {
        content: [{ type: "text", text: `Config "${key}" already set to "${value}". No change needed.` }]
      };
    }

    await setConfig(key, value);
    return {
      content: [{ type: "text", text: `Config "${key}" set to "${value}".` }]
    };
  }
);

Anti-Patterns

  • Tool names longer than 64 characters (may be truncated or rejected)
  • Missing
    required
    array in raw JSON Schema (Claude may skip required fields)
  • Descriptions that say "This tool does X" instead of "Use this when..."
  • Returning HTML or binary data as text content (use appropriate MIME types)
  • Tools with more than 10-15 parameters (split into multiple focused tools)
  • Not including pagination for tools that can return unbounded results
  • Destructive tools without confirmation (delete, drop, reset)
  • Using generic error messages like "Something went wrong" without actionable guidance

Quick Reference

AspectGuideline
Name format
snake_case
, verb_noun, max 64 chars
DescriptionExplain WHEN to use, not just what it does
ParametersInclude type, description, defaults, constraints
Required fieldsAlways specify in schema
PaginationCursor-based, 20-50 items per page
ErrorsUse
isError: true
, give actionable messages
IdempotencySame input = same result for write operations
Tool countPrefer many focused tools over few large ones

Description template:

"[Action description]. Use when [trigger conditions]. [Constraints/limits]. [Returns description]."