Tool Integration

Overview

Claude Code’s tool system allows it to interact with your file system and execute commands. Additionally, the Model Context Protocol (MCP) enables you to create custom tools that extend Claude’s capabilities.

Key Concepts

Tool Definition

Tools in Claude Code are defined with:

  • Type-safe parameter schemas
  • Runtime validation
  • Composable execution patterns

Tool Integration Pattern

// Tool integration pattern for type-safe tools
interface Tool<TParams = any, TResult = any> {
  name: string;
  description: string;
  parameters: z.ZodSchema<TParams>;
  execute: (params: TParams) => Promise<TResult>;
}
 
class ToolRegistry {
  private tools = new Map<string, Tool>();
 
  register<TParams, TResult>(tool: Tool<TParams, TResult>): void {
    this.tools.set(tool.name, tool);
  }
 
  async execute(name: string, params: unknown): Promise<unknown> {
    const tool = this.tools.get(name);
    if (!tool) {
      throw new Error(`Tool ${name} not found`);
    }
 
    const validatedParams = tool.parameters.parse(params);
    return tool.execute(validatedParams);
  }
 
  getToolSchemas(): Array<{name: string; description: string; parameters: any}> {
    return Array.from(this.tools.values()).map(tool => ({
      name: tool.name,
      description: tool.description,
      parameters: zodToJsonSchema(tool.parameters),
    }));
  }
}
 
// Example tool implementation
const searchTool: Tool<{query: string}, {results: string[]}> = {
  name: 'search',
  description: 'Search for information',
  parameters: z.object({
    query: z.string().min(1),
  }),
  async execute(params) {
    // Implement search logic
    return { results: [`Result for: ${params.query}`] };
  },
};

Key Benefits

  • Type Safety: Compile-time and runtime validation
  • Dynamic Management: Registry pattern for flexible tool loading
  • Composability: Chain tools for complex workflows

Built-in Tools

1. File Operations Tool

The str_replace_editor_tool provides file manipulation capabilities:

// Tool use appears in assistant messages
interface ToolUseBlock {
  type: 'tool_use';
  id: string;
  name: 'str_replace_editor_tool';
  input: {
    command: 'view' | 'str_replace' | 'create';
    path: string;
    // Additional fields based on command
  };
}

View Command

// Reading files
interface ViewCommand {
  command: 'view';
  path: string;
  view_range?: [number, number]; // Optional line range
}
 
// Example usage in a prompt
const readFilePrompt = `
Please read the file at src/index.ts and analyze its structure.
`;

Create Command

// Creating new files
interface CreateCommand {
  command: 'create';
  path: string;
  file_text: string;
}
 
// Example usage
const createFilePrompt = `
Create a new TypeScript configuration file at tsconfig.json with strict settings.
`;

String Replace Command

// Editing existing files
interface StrReplaceCommand {
  command: 'str_replace';
  path: string;
  old_str: string;
  new_str: string;
}
 
// Example usage
const editFilePrompt = `
In the file src/app.ts, replace the function "oldFunction" with a new async version.
`;

2. Bash Command Tool

Execute shell commands:

interface BashToolUse {
  type: 'tool_use';
  id: string;
  name: 'bash';
  input: {
    command: string;
  };
}
 
// Example usage
const runTestsPrompt = `
Run the test suite and show me the results.
`;

Parsing Tool Uses from Responses

import { query, type SDKMessage } from "@anthropic-ai/claude-code";
 
interface ToolExecution {
  tool: string;
  input: any;
  result?: any;
  error?: string;
}
 
async function extractToolUses(prompt: string): Promise<ToolExecution[]> {
  const executions: ToolExecution[] = [];
  
  for await (const message of query({
    prompt,
    abortController: new AbortController()
  })) {
    if (message.type === 'assistant') {
      const content = message.data.message.content;
      
      if (Array.isArray(content)) {
        for (const block of content) {
          if (block.type === 'tool_use') {
            executions.push({
              tool: block.name,
              input: block.input
            });
          } else if (block.type === 'tool_result') {
            // Match result with its tool use
            const execution = executions.find(
              e => e.tool === block.tool_use_id
            );
            if (execution) {
              execution.result = block.content;
              execution.error = block.is_error ? block.content : undefined;
            }
          }
        }
      }
    }
  }
  
  return executions;
}

Model Context Protocol (MCP)

MCP allows you to create custom tools that Claude can use. Here’s how to build and integrate MCP servers:

Creating an MCP Server

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
 
// Create server instance
const server = new McpServer({
  name: "my-tools-server",
  version: "1.0.0",
  description: "Custom tools for Claude"
});
 
// Define a custom tool
server.tool(
  "database-query",
  {
    description: "Execute a database query",
    inputSchema: z.object({
      query: z.string().describe("SQL query to execute"),
      database: z.string().describe("Database name")
    })
  },
  async ({ query, database }) => {
    try {
      // Your database logic here
      const result = await executeQuery(query, database);
      
      return {
        content: [{
          type: "text",
          text: JSON.stringify(result, null, 2)
        }]
      };
    } catch (error) {
      return {
        content: [{
          type: "text",
          text: `Error: ${error.message}`
        }],
        isError: true
      };
    }
  }
);
 
// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);

Advanced MCP Server with Multiple Tools

import { promises as fs } from 'fs';
import { exec } from 'child_process';
import { promisify } from 'util';
 
const execAsync = promisify(exec);
 
// Tool 1: JSON file operations
server.tool(
  "json-file-ops",
  {
    description: "Read, write, or modify JSON files",
    inputSchema: z.object({
      operation: z.enum(['read', 'write', 'update']),
      path: z.string(),
      data: z.any().optional(),
      jsonPath: z.string().optional()
    })
  },
  async ({ operation, path, data, jsonPath }) => {
    switch (operation) {
      case 'read':
        const content = await fs.readFile(path, 'utf-8');
        const json = JSON.parse(content);
        return {
          content: [{
            type: "text",
            text: JSON.stringify(json, null, 2)
          }]
        };
        
      case 'write':
        await fs.writeFile(path, JSON.stringify(data, null, 2));
        return {
          content: [{
            type: "text",
            text: `Successfully wrote to ${path}`
          }]
        };
        
      case 'update':
        const existing = JSON.parse(await fs.readFile(path, 'utf-8'));
        // Simple jsonPath implementation
        const keys = jsonPath.split('.');
        let target = existing;
        for (let i = 0; i < keys.length - 1; i++) {
          target = target[keys[i]];
        }
        target[keys[keys.length - 1]] = data;
        
        await fs.writeFile(path, JSON.stringify(existing, null, 2));
        return {
          content: [{
            type: "text",
            text: `Updated ${jsonPath} in ${path}`
          }]
        };
    }
  }
);
 
// Tool 2: Git operations
server.tool(
  "git-ops",
  {
    description: "Perform git operations",
    inputSchema: z.object({
      command: z.enum(['status', 'diff', 'commit', 'log']),
      message: z.string().optional(),
      files: z.array(z.string()).optional()
    })
  },
  async ({ command, message, files }) => {
    let gitCommand = '';
    
    switch (command) {
      case 'status':
        gitCommand = 'git status --porcelain';
        break;
      case 'diff':
        gitCommand = 'git diff --cached';
        break;
      case 'commit':
        if (files) {
          await execAsync(`git add ${files.join(' ')}`);
        }
        gitCommand = `git commit -m "${message}"`;
        break;
      case 'log':
        gitCommand = 'git log --oneline -10';
        break;
    }
    
    const { stdout, stderr } = await execAsync(gitCommand);
    
    return {
      content: [{
        type: "text",
        text: stdout || stderr
      }],
      isError: !!stderr
    };
  }
);
 
// Tool 3: API testing
server.tool(
  "api-test",
  {
    description: "Test REST API endpoints",
    inputSchema: z.object({
      url: z.string(),
      method: z.enum(['GET', 'POST', 'PUT', 'DELETE']),
      headers: z.record(z.string()).optional(),
      body: z.any().optional()
    })
  },
  async ({ url, method, headers, body }) => {
    const response = await fetch(url, {
      method,
      headers: {
        'Content-Type': 'application/json',
        ...headers
      },
      body: body ? JSON.stringify(body) : undefined
    });
    
    const responseBody = await response.json();
    
    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          status: response.status,
          headers: Object.fromEntries(response.headers),
          body: responseBody
        }, null, 2)
      }]
    };
  }
);

MCP Resources

Resources provide data that Claude can access:

// Provide configuration data
server.resource(
  "config://app",
  async () => {
    const config = await loadAppConfiguration();
    
    return {
      contents: [{
        uri: "config://app",
        mimeType: "application/json",
        text: JSON.stringify(config, null, 2)
      }]
    };
  }
);
 
// Provide environment information
server.resource(
  "env://variables",
  async () => {
    const safeEnv = Object.entries(process.env)
      .filter(([key]) => !key.includes('SECRET') && !key.includes('KEY'))
      .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
    
    return {
      contents: [{
        uri: "env://variables",
        mimeType: "application/json",
        text: JSON.stringify(safeEnv, null, 2)
      }]
    };
  }
);
 
// Provide database schema
server.resource(
  "schema://database",
  async () => {
    const schema = await getDatabaseSchema();
    
    return {
      contents: [{
        uri: "schema://database",
        mimeType: "text/plain",
        text: schema
      }]
    };
  }
);

MCP Prompts

Define reusable prompt templates:

// Code review prompt
server.prompt(
  "code-review",
  {
    description: "Perform a comprehensive code review",
    inputSchema: z.object({
      files: z.array(z.string()).describe("Files to review"),
      focus: z.enum(['security', 'performance', 'style', 'all']).optional()
    })
  },
  ({ files, focus = 'all' }) => ({
    messages: [{
      role: "user",
      content: {
        type: "text",
        text: `Please perform a ${focus} code review on the following files: ${files.join(', ')}
        
        Focus areas:
        ${focus === 'all' || focus === 'security' ? '- Security vulnerabilities' : ''}
        ${focus === 'all' || focus === 'performance' ? '- Performance optimizations' : ''}
        ${focus === 'all' || focus === 'style' ? '- Code style and best practices' : ''}
        
        Provide specific line numbers and actionable suggestions.`
      }
    }]
  })
);
 
// Test generation prompt
server.prompt(
  "generate-tests",
  {
    description: "Generate unit tests for code",
    inputSchema: z.object({
      file: z.string(),
      framework: z.enum(['jest', 'mocha', 'vitest']).default('jest')
    })
  },
  ({ file, framework }) => ({
    messages: [{
      role: "user",
      content: {
        type: "text",
        text: `Generate comprehensive unit tests for ${file} using ${framework}.
        
        Requirements:
        - Test all exported functions and classes
        - Include edge cases and error scenarios
        - Use proper mocking for dependencies
        - Aim for >80% code coverage
        - Follow ${framework} best practices`
      }
    }]
  })
);

Configuring MCP in Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "my-tools": {
      "command": "node",
      "args": ["/path/to/your/mcp-server.js"],
      "env": {
        "DATABASE_URL": "postgresql://localhost/mydb"
      }
    },
    "file-tools": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
    },
    "github-tools": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..."
      }
    }
  }
}

Using Tools in Your Application

1. Tool Usage Monitor

class ToolUsageMonitor {
  private toolUsage: Map<string, number> = new Map();
  
  async monitorQuery(prompt: string) {
    for await (const message of query({
      prompt,
      abortController: new AbortController()
    })) {
      if (message.type === 'assistant') {
        const content = message.data.message.content;
        
        if (Array.isArray(content)) {
          for (const block of content) {
            if (block.type === 'tool_use') {
              const count = this.toolUsage.get(block.name) || 0;
              this.toolUsage.set(block.name, count + 1);
              
              console.log(`Tool used: ${block.name}`);
              console.log(`Input:`, JSON.stringify(block.input, null, 2));
            }
          }
        }
      }
    }
  }
  
  getUsageStats() {
    return Object.fromEntries(this.toolUsage);
  }
}

2. Tool Result Validator

class ToolResultValidator {
  validateFileOperation(result: any): boolean {
    // Validate file operations completed successfully
    if (result.error) {
      console.error('File operation failed:', result.error);
      return false;
    }
    
    // Check for common issues
    if (result.content?.includes('Permission denied')) {
      console.error('Permission issue detected');
      return false;
    }
    
    return true;
  }
  
  validateBashCommand(result: any): boolean {
    // Check exit codes and stderr
    if (result.exitCode && result.exitCode !== 0) {
      console.error('Command failed with exit code:', result.exitCode);
      return false;
    }
    
    return true;
  }
}

3. Custom Tool Wrapper

class CustomToolWrapper {
  async executeWithTools(
    prompt: string,
    customTools: Map<string, (input: any) => Promise<any>>
  ) {
    const results: any[] = [];
    
    for await (const message of query({
      prompt,
      abortController: new AbortController()
    })) {
      if (message.type === 'assistant') {
        const content = message.data.message.content;
        
        if (Array.isArray(content)) {
          for (const block of content) {
            if (block.type === 'tool_use' && customTools.has(block.name)) {
              try {
                const toolFn = customTools.get(block.name)!;
                const result = await toolFn(block.input);
                results.push({
                  tool: block.name,
                  input: block.input,
                  result
                });
              } catch (error) {
                console.error(`Tool ${block.name} failed:`, error);
              }
            }
          }
        }
      }
    }
    
    return results;
  }
}

Advanced Patterns

Tool Composition

Create complex workflows by composing multiple tools:

class ToolChain {
  constructor(private registry: ToolRegistry) {}
 
  async execute(steps: Array<{tool: string; params: any}>): Promise<any[]> {
    const results = [];
    for (const step of steps) {
      const result = await this.registry.execute(step.tool, step.params);
      results.push(result);
    }
    return results;
  }
}

Error Handling

Implement robust error handling for tool execution:

class SafeToolRegistry extends ToolRegistry {
  async execute(name: string, params: unknown): Promise<unknown> {
    try {
      return await super.execute(name, params);
    } catch (error) {
      if (error instanceof z.ZodError) {
        throw new ToolValidationError(name, error);
      }
      throw new ToolExecutionError(name, error);
    }
  }
}

Best Practices

  1. Clear Tool Descriptions: Improve AI tool selection with descriptive names and documentation
  2. Parameter Validation: Use Zod schemas for comprehensive validation
  3. Error Context: Provide detailed error messages for debugging
  4. Tool Versioning: Implement versioning for compatibility
  5. Result Caching: Cache tool results when appropriate
  6. Tool Safety: Always validate tool inputs and outputs
  7. Permission Control: Use appropriate permission modes
  8. Rate Limiting: Be mindful of tool execution frequency
  9. Logging: Log all tool uses for debugging and auditing
  10. Testing: Test MCP servers thoroughly before deployment
  11. Security: Never expose sensitive operations through tools

Troubleshooting

Common Issues

Circular Dependencies

Problem: Tools referencing each other cause TypeScript compilation errors.

Solution: Use lazy loading and interface segregation:

// Use interfaces instead of concrete types
interface ISearchTool {
  search(query: string): Promise<string[]>;
}
 
// Lazy load dependencies
const getSearchTool = () => require('./search-tool').searchTool;

Type Inference Issues

Problem: TypeScript cannot infer tool parameter types.

Solution: Use explicit type parameters:

registry.register<SearchParams, SearchResult>(searchTool);

Next Steps


Tags: claude-code typescript tools mcp integration automation sdk