# MCP Tool Builder

> Guides implementation of new MCP tools from specification to production-ready code, following the braiins-pool-mcp-server architecture patterns and best practices defined in ARCHITECTURE.md.

- Skill: `majiayu000/mcp-tool-builder` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/mcp-tool-builder`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/mcp-tool-builder/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/mcp-tool-builder

---


# MCP Tool Builder Skill

## Description

Implement new MCP tools for the braiins-pool-mcp-server from specification to production-ready code. This skill enforces the project's architectural patterns: cache-first data access, Zod validation, comprehensive error handling, and >80% test coverage.

## When to Use This Skill

- When implementing a new MCP tool from API.md specification
- When adding functionality to query Braiins Pool API
- When the user asks to "create", "build", or "implement" a tool
- After completing the design phase for a new tool
- When converting API endpoints to MCP tools

## When NOT to Use This Skill

- When refactoring existing tools (use refactoring workflow)
- When only updating schemas (use mcp-schema-designer)
- When debugging issues (use root-cause-tracing)
- When writing documentation only (use scribe-role-skill)

## Prerequisites

- [ ] API.md contains the endpoint specification
- [ ] ARCHITECTURE.md is available for patterns
- [ ] TypeScript project is initialized (package.json exists)
- [ ] Redis is configured (for caching)
- [ ] Test framework is set up (vitest/jest)

---

## Workflow

### Phase 1: Specification Review

**Step 1.1: Read API Documentation**

```bash
# Review the API specification
cat API.md | grep -A 50 "{endpoint_path}"
```

Extract and document:
- HTTP method (GET/POST)
- Endpoint path with parameters
- Query parameters and their types
- Response schema
- Authentication requirements
- Rate limiting constraints

**Step 1.2: Determine Tool Requirements**

Fill out this specification template:

```markdown
## Tool Specification: {toolName}

**MCP Tool Name**: {camelCase name, e.g., getMinerStats}
**API Endpoint**: {method} {path}
**Cache TTL**: {seconds}
**Priority**: {P0/P1/P2}

### Input Parameters
| Name | Type | Required | Validation |
|------|------|----------|------------|
| {param} | {type} | {yes/no} | {constraints} |

### Output Format
- Content Type: text (JSON stringified)
- Error Handling: {specific error cases}

### Caching Strategy
- Cache Key: braiins:{resource}:{identifiers}
- TTL: {seconds}
- Invalidation: {conditions}
```

---

### Phase 2: Schema Definition

**Step 2.1: Create Input Schema**

Create file: `src/schemas/{toolName}Input.ts`

```typescript
import { z } from 'zod';

/**
 * Input schema for {toolName} MCP tool
 *
 * @example
 * {
 *   paramName: "example_value"
 * }
 */
export const {ToolName}InputSchema = z.object({
  // Required parameters
  requiredParam: z.string()
    .min(1, '{Param} is required')
    .max(100, '{Param} too long')
    .regex(/^[a-zA-Z0-9\-_]+$/, 'Invalid {param} format'),

  // Optional parameters with defaults
  optionalParam: z.number()
    .int()
    .min(1)
    .max(1000)
    .default(50),
});

export type {ToolName}Input = z.infer<typeof {ToolName}InputSchema>;
```

**Step 2.2: Create Response Schema**

Create file: `src/schemas/{toolName}Response.ts`

```typescript
import { z } from 'zod';

/**
 * API response schema for {endpoint}
 * Based on API.md Section {X.Y}
 */
export const {ToolName}ResponseSchema = z.object({
  // Define all response fields with types
  field1: z.string(),
  field2: z.number(),
  nested: z.object({
    subField: z.string(),
  }),
  timestamp: z.string().datetime(),
});

export type {ToolName}Response = z.infer<typeof {ToolName}ResponseSchema>;
```

---

### Phase 3: Handler Implementation

**Step 3.1: Create Tool File**

Create file: `src/tools/{toolName}.ts`

```typescript
import { z } from 'zod';
import { {ToolName}InputSchema, type {ToolName}Input } from '../schemas/{toolName}Input';
import { {ToolName}ResponseSchema } from '../schemas/{toolName}Response';
import { braiinsClient } from '../api/braiinsClient';
import { redisManager } from '../cache/redisManager';
import { BraiinsApiError, CacheError } from '../utils/errors';
import { logger } from '../utils/logger';

/**
 * MCP Tool: {toolName}
 *
 * {Description of what the tool does}
 *
 * @see API.md Section {X.Y}
 */
export const {toolName}Tool = {
  name: '{toolName}',
  description: '{User-friendly description for AI model}',
  inputSchema: {ToolName}InputSchema,

  handler: async (rawInput: unknown) => {
    // Step 1: Validate input
    const parseResult = {ToolName}InputSchema.safeParse(rawInput);
    if (!parseResult.success) {
      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            error: 'VALIDATION_ERROR',
            message: 'Invalid input parameters',
            details: parseResult.error.flatten(),
          }),
        }],
        isError: true,
      };
    }
    const input = parseResult.data;

    // Step 2: Generate cache key
    const cacheKey = `braiins:{resource}:${input.requiredParam}`;

    // Step 3: Check cache
    try {
      const cached = await redisManager.get<{ToolName}Response>(cacheKey);
      if (cached) {
        logger.debug('Cache hit', { tool: '{toolName}', key: cacheKey });
        return {
          content: [{
            type: 'text' as const,
            text: JSON.stringify(cached),
          }],
        };
      }
    } catch (error) {
      // Log but don't fail on cache errors
      logger.warn('Cache error, falling through to API', { error });
    }

    // Step 4: Call API
    try {
      const response = await braiinsClient.{apiMethod}(input);

      // Step 5: Validate response
      const validated = {ToolName}ResponseSchema.parse(response);

      // Step 6: Cache result
      try {
        await redisManager.set(cacheKey, validated, {TTL_SECONDS});
      } catch (cacheError) {
        logger.warn('Failed to cache result', { error: cacheError });
      }

      // Step 7: Return formatted response
      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify(validated),
        }],
      };
    } catch (error) {
      // Handle specific error types
      if (error instanceof BraiinsApiError) {
        return {
          content: [{
            type: 'text' as const,
            text: JSON.stringify({
              error: error.code,
              message: error.message,
              statusCode: error.statusCode,
            }),
          }],
          isError: true,
        };
      }

      // Unknown error
      logger.error('Unexpected error in {toolName}', { error });
      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            error: 'INTERNAL_ERROR',
            message: 'An unexpected error occurred',
          }),
        }],
        isError: true,
      };
    }
  },
};
```

---

### Phase 4: Testing

**Step 4.1: Create Unit Tests**

Create file: `tests/unit/tools/{toolName}.test.ts`

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { {toolName}Tool } from '../../../src/tools/{toolName}';
import { braiinsClient } from '../../../src/api/braiinsClient';
import { redisManager } from '../../../src/cache/redisManager';

// Mock dependencies
vi.mock('../../../src/api/braiinsClient');
vi.mock('../../../src/cache/redisManager');

describe('{toolName} Tool', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('Input Validation', () => {
    it('should reject missing required parameter', async () => {
      const result = await {toolName}Tool.handler({});
      expect(result.isError).toBe(true);
      expect(JSON.parse(result.content[0].text)).toMatchObject({
        error: 'VALIDATION_ERROR',
      });
    });

    it('should reject invalid parameter format', async () => {
      const result = await {toolName}Tool.handler({
        requiredParam: 'invalid@format!',
      });
      expect(result.isError).toBe(true);
    });

    it('should accept valid parameters', async () => {
      vi.mocked(redisManager.get).mockResolvedValue(null);
      vi.mocked(braiinsClient.{apiMethod}).mockResolvedValue({
        // Valid response
      });

      const result = await {toolName}Tool.handler({
        requiredParam: 'valid-id',
      });
      expect(result.isError).toBeFalsy();
    });
  });

  describe('Caching', () => {
    it('should return cached data on cache hit', async () => {
      const cachedData = { /* mock cached response */ };
      vi.mocked(redisManager.get).mockResolvedValue(cachedData);

      const result = await {toolName}Tool.handler({
        requiredParam: 'test-id',
      });

      expect(braiinsClient.{apiMethod}).not.toHaveBeenCalled();
      expect(JSON.parse(result.content[0].text)).toEqual(cachedData);
    });

    it('should call API on cache miss', async () => {
      vi.mocked(redisManager.get).mockResolvedValue(null);
      vi.mocked(braiinsClient.{apiMethod}).mockResolvedValue({
        // API response
      });

      await {toolName}Tool.handler({ requiredParam: 'test-id' });

      expect(braiinsClient.{apiMethod}).toHaveBeenCalled();
    });

    it('should cache API response', async () => {
      vi.mocked(redisManager.get).mockResolvedValue(null);
      const apiResponse = { /* mock response */ };
      vi.mocked(braiinsClient.{apiMethod}).mockResolvedValue(apiResponse);

      await {toolName}Tool.handler({ requiredParam: 'test-id' });

      expect(redisManager.set).toHaveBeenCalledWith(
        expect.stringContaining('braiins:'),
        expect.anything(),
        {TTL_SECONDS}
      );
    });
  });

  describe('Error Handling', () => {
    it('should handle API errors gracefully', async () => {
      vi.mocked(redisManager.get).mockResolvedValue(null);
      vi.mocked(braiinsClient.{apiMethod}).mockRejectedValue(
        new BraiinsApiError('Not found', 404)
      );

      const result = await {toolName}Tool.handler({
        requiredParam: 'nonexistent',
      });

      expect(result.isError).toBe(true);
      expect(JSON.parse(result.content[0].text)).toMatchObject({
        statusCode: 404,
      });
    });

    it('should fall through on cache errors', async () => {
      vi.mocked(redisManager.get).mockRejectedValue(new Error('Redis down'));
      vi.mocked(braiinsClient.{apiMethod}).mockResolvedValue({
        // API response
      });

      const result = await {toolName}Tool.handler({
        requiredParam: 'test-id',
      });

      expect(result.isError).toBeFalsy();
    });
  });
});
```

**Step 4.2: Run Tests**

```bash
npm test -- --coverage tests/unit/tools/{toolName}.test.ts
```

Target: >80% coverage for the tool file.

---

### Phase 5: Integration

**Step 5.1: Export Tool**

Update `src/tools/index.ts`:

```typescript
export { {toolName}Tool } from './{toolName}';
```

**Step 5.2: Register with MCP Server**

Update `src/index.ts`:

```typescript
import { {toolName}Tool } from './tools';

// In server initialization
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    // ... existing tools
    {
      name: {toolName}Tool.name,
      description: {toolName}Tool.description,
      inputSchema: zodToJsonSchema({toolName}Tool.inputSchema),
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  switch (request.params.name) {
    // ... existing cases
    case '{toolName}':
      return {toolName}Tool.handler(request.params.arguments);
  }
});
```

**Step 5.3: Final Verification**

```bash
# Run full test suite
npm test

# Run type check
npm run type-check

# Run linter
npm run lint
```

---

## Examples

### Example 1: Implementing getUserOverview Tool

**Input**: "Create the getUserOverview MCP tool based on API.md Section 5.1"

**Process**:

1. **Specification Review**:
   - Endpoint: GET /user/overview
   - No input parameters (uses auth token)
   - Returns: hashrate, rewards, worker counts
   - Cache TTL: 30s

2. **Schema Creation**:
```typescript
// src/schemas/getUserOverviewInput.ts
export const GetUserOverviewInputSchema = z.object({
  // No parameters needed - uses bearer token auth
});

// src/schemas/getUserOverviewResponse.ts
export const GetUserOverviewResponseSchema = z.object({
  username: z.string(),
  currency: z.string(),
  hashrate: z.object({
    current: z.number(),
    avg_1h: z.number(),
    avg_24h: z.number(),
  }),
  rewards: z.object({
    confirmed: z.string(),
    unconfirmed: z.string(),
    last_payout: z.string(),
    last_payout_at: z.string().datetime(),
  }),
  workers: z.object({
    active: z.number(),
    inactive: z.number(),
    total: z.number(),
  }),
  updated_at: z.string().datetime(),
});
```

3. **Handler Implementation**: Following template with TTL=30

4. **Tests**: 12 tests covering validation, caching, errors

5. **Integration**: Registered in index.ts

---

### Example 2: Implementing listWorkers Tool with Pagination

**Input**: "Create the listWorkers tool with pagination support"

**Specification**:
- Endpoint: GET /workers
- Parameters: page, pageSize, status, search, sortBy
- Cache TTL: 30s (varies by filters)

**Key Implementation Differences**:

1. **Cache Key includes filter hash**:
```typescript
const filtersHash = hashObject({ status, search, sortBy });
const cacheKey = `braiins:workers:list:${page}:${filtersHash}`;
```

2. **Pagination in response**:
```typescript
return {
  content: [{
    type: 'text',
    text: JSON.stringify({
      data: validated.workers,
      pagination: {
        page: validated.page,
        pageSize: validated.page_size,
        total: validated.total,
        hasMore: validated.page * validated.page_size < validated.total,
      },
    }),
  }],
};
```

---

## Quality Standards

Every tool implemented with this skill must meet:

- [ ] Input schema validates all parameters
- [ ] Response schema matches API.md specification
- [ ] Cache-first pattern implemented correctly
- [ ] All error cases handled (401, 403, 404, 429, 500)
- [ ] Logging at appropriate levels
- [ ] Unit tests >80% coverage
- [ ] Integration tests for happy path
- [ ] No sensitive data in logs or errors
- [ ] Type-safe throughout (no `any` types)

---

## Common Pitfalls

**Pitfall 1: Forgetting to sanitize cache keys**
```typescript
// BAD: User input directly in cache key
const cacheKey = `braiins:worker:${input.workerId}`;

// GOOD: Sanitize or hash user input
const sanitized = input.workerId.replace(/[^a-zA-Z0-9\-_]/g, '');
const cacheKey = `braiins:worker:${sanitized}`;
```

**Pitfall 2: Not handling cache errors**
```typescript
// BAD: Cache error breaks the request
const cached = await redisManager.get(cacheKey);

// GOOD: Fall through on cache errors
try {
  const cached = await redisManager.get(cacheKey);
  if (cached) return formatResponse(cached);
} catch {
  logger.warn('Cache unavailable, calling API directly');
}
```

**Pitfall 3: Exposing internal errors**
```typescript
// BAD: Leaking stack traces
return { error: error.stack };

// GOOD: User-friendly error message
return { error: 'INTERNAL_ERROR', message: 'An unexpected error occurred' };
```

---

## Troubleshooting

**Issue**: Tool not appearing in Claude's available tools
**Solution**:
1. Check tool is exported from `src/tools/index.ts`
2. Verify tool is registered in `src/index.ts`
3. Restart MCP server after changes

**Issue**: Tests failing with "Cannot find module"
**Solution**:
1. Check import paths are correct
2. Run `npm run build` to compile TypeScript
3. Verify file exists at expected path

**Issue**: Cache always misses
**Solution**:
1. Verify Redis connection in logs
2. Check cache key generation is consistent
3. Confirm TTL is set correctly

---

## Version History

- **1.0.0** (2025-12-18): Initial skill definition for braiins-pool-mcp-server

---

## References

- [ARCHITECTURE.md](../../../ARCHITECTURE.md) - System architecture patterns
- [API.md](../../../API.md) - Braiins API specification
- [MCP Protocol Spec](https://modelcontextprotocol.io/) - MCP standard

