MCP Server Tool Design and Naming Best Practices: A Complete Guide

Use snake_case tool names prefixed with the service name, implement strict Pydantic or Zod validation, and provide dual JSON/Markdown response formats with proper pagination metadata.

The Anthropic Skills repository provides authoritative guidelines for building Model Context Protocol (MCP) servers that are discoverable, composable, and safe for AI agents. Following the conventions established in skills/mcp-builder/reference/mcp_best_practices.md ensures your tools integrate seamlessly with Claude and other MCP clients.

Server Naming Conventions

Consistent server naming prevents collisions and clarifies the service domain.

Python Servers

Use the pattern {service}_mcp (lowercase with underscores) as defined in skills/mcp-builder/reference/python_mcp_server.md.

  • Examples: slack_mcp, github_mcp, jira_mcp
from mcp.server.fastmcp import FastMCP

# Correct naming pattern

mcp = FastMCP("slack_mcp")

Node.js/TypeScript Servers

Use the pattern {service}-mcp-server (lowercase with hyphens) as specified in skills/mcp-builder/reference/node_mcp_server.md.

  • Examples: slack-mcp-server, github-mcp-server
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const server = new McpServer({ 
  name: "github-mcp-server", 
  version: "1.0.0" 
});

Tool Naming Best Practices

Clear tool names reduce ambiguity for AI agents deciding which function to invoke.

Use Snake Case with Service Prefixes

All tool names must use snake_case (search_users, create_project) and include the service prefix to avoid namespace collisions.

  • Correct: slack_send_message, github_create_issue
  • Incorrect: send_message, create_issue

Start with Action Verbs

Begin tool names with standard action verbs that indicate the operation type:

  • get / list – Retrieve single or multiple items
  • create – Instantiate new resources
  • update – Modify existing resources
  • delete – Remove resources

Tool Design Fundamentals

Robust tool design requires strict validation, comprehensive metadata, and safety annotations.

Strict Input Validation

Use Pydantic (Python) or Zod (TypeScript) with .strict() configuration to prevent extra fields and validate types automatically.

from pydantic import BaseModel, Field, ConfigDict

class SearchInput(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
    
    query: str = Field(..., min_length=2, max_length=200)
    limit: int = Field(default=20, ge=1, le=100)
import { z } from "zod";

const IssueSearchSchema = z.object({
  query: z.string().min(2).max(200),
  limit: z.number().int().min(1).max(100).default(20),
  offset: z.number().int().min(0).default(0),
  response_format: z.enum(["markdown", "json"]).default("markdown"),
}).strict();

Tool Annotations

Provide annotations in skills/mcp-builder/reference/mcp_best_practices.md to guide agent behavior:

  • readOnlyHint: true if the tool only reads data
  • destructiveHint: true if the tool deletes or irreversibly modifies data
  • idempotentHint: true if repeated calls with the same inputs produce the same results
  • openWorldHint: true if the tool interacts with external systems beyond the local environment
@mcp.tool(
    name="slack_search_messages",
    annotations={
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True,
    },
)
async def slack_search_messages(params: SearchInput) -> str:
    """Search Slack messages matching query. Returns JSON or Markdown."""
    ...

Response Handling and Pagination

Dual-format responses and consistent pagination metadata improve usability across different client types.

Dual Response Formats

Support both JSON (machine-readable) and Markdown (human-readable) outputs. Default to Markdown for general agent usage unless the client specifically requests JSON.

if params.response_format == SearchInput.ResponseFormat.JSON:
    return json.dumps({
        "total": total,
        "count": len(messages),
        "offset": params.offset,
        "messages": messages,
        "has_more": total > (params.offset + len(messages)),
    }, indent=2)

# Markdown formatting for human readability

lines = [f"# Search Results", ""]

for msg in messages:
    lines.append(f"**{msg['user']}**: {msg['text']}")
return "\n".join(lines)

Pagination Metadata

For list operations, return standard pagination fields as defined in skills/mcp-builder/reference/mcp_best_practices.md:

  • has_more: Boolean indicating additional results available
  • next_offset: Integer for the next page starting position
  • total_count: Total number of matching items
  • Default limit: 20-50 items per request

Transport and Error Handling

Proper transport selection and sanitized error messages ensure reliable operation.

Transport Selection

Choose transport based on deployment context:

  • stdio: Use for local CLI tools and single-user environments
  • streamable HTTP: Use for remote services requiring multiple concurrent clients
  • Avoid SSE: Server-Sent Events transport is deprecated in favor of streamable HTTP
// stdio transport for local usage
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
await server.connect(new StdioServerTransport());

Error Handling Best Practices

Map API errors to user-friendly messages without exposing internal stack traces or sensitive tokens:

function handleApiError(error: unknown): string {
  if (error instanceof AxiosError) {
    if (error.response) {
      switch (error.response.status) {
        case 404: return "Error: Resource not found.";
        case 403: return "Error: Permission denied.";
        case 429: return "Error: Rate limit exceeded.";
        default: return `Error: API request failed with status ${error.response.status}`;
      }
    }
  }
  return `Error: ${error instanceof Error ? error.message : String(error)}`;
}

Code Quality and Testing

Maintainable MCP servers require modular architecture and comprehensive validation.

Code Reusability

Extract common functionality into shared modules:

  • API clients: Reusable HTTP clients with authentication
  • Pagination helpers: Standardized offset/limit handling
  • Formatters: Shared JSON and Markdown rendering utilities
  • Error handlers: Centralized exception mapping

Testing Requirements

As specified in skills/mcp-builder/reference/mcp_best_practices.md, validate your server with:

  • Functional tests: Verify individual tool logic
  • Integration tests: Test against live or mocked APIs
  • Security tests: Ensure no token leakage in errors
  • Build verification: Confirm npm run build (Node) or python -m <module> (Python) executes without errors

Summary

  • Name servers using {service}_mcp (Python) or {service}-mcp-server (Node) patterns
  • Name tools with snake_case, service prefixes, and action verbs (get, list, create, update, delete)
  • Validate inputs strictly using Pydantic (Python) or Zod (Node) with .strict() configuration
  • Annotate tools with readOnlyHint, destructiveHint, idempotentHint, and openWorldHint
  • Support dual formats (JSON and Markdown) with standard pagination metadata (has_more, next_offset, total_count)
  • Choose transport based on context: stdio for local tools, streamable HTTP for remote services
  • Handle errors gracefully without exposing stack traces or sensitive credentials

Frequently Asked Questions

What is the correct naming convention for MCP server packages?

Python servers should use the pattern {service}_mcp with lowercase and underscores (e.g., slack_mcp, github_mcp), while Node.js/TypeScript servers should use {service}-mcp-server with lowercase and hyphens (e.g., slack-mcp-server). These patterns are defined in skills/mcp-builder/reference/mcp_best_practices.md and prevent naming collisions across the MCP ecosystem.

How should I handle input validation in MCP tools?

Use strict schema validation with Pydantic for Python or Zod for TypeScript, ensuring you configure .strict() or extra="forbid" to reject unexpected fields. Define clear constraints using Field() or .min()/.max() validators, and include descriptive metadata so the LLM understands parameter purposes. This approach is documented in skills/mcp-builder/reference/python_mcp_server.md and skills/mcp-builder/reference/node_mcp_server.md.

What transport protocol should I choose for my MCP server?

Select stdio transport for local CLI tools and single-user environments where the server runs as a subprocess, or streamable HTTP for remote services that must support multiple concurrent clients over the network. Avoid using Server-Sent Events (SSE) as it is deprecated in favor of streamable HTTP. The transport selection guidance appears in skills/mcp-builder/reference/mcp_best_practices.md.

Why should MCP tools support both JSON and Markdown response formats?

Supporting dual formats accommodates different consumption patterns: JSON provides structured data for programmatic processing and agent tool chaining, while Markdown offers human-readable output for direct user display. Default to Markdown unless the client explicitly requests JSON via a parameter. This pattern is implemented in the reference examples within skills/mcp-builder/reference/python_mcp_server.md and ensures maximum compatibility across diverse MCP clients.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →