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.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
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
# 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:
## 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
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
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
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
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
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:
export { {toolName}Tool } from './{toolName}';
Step 5.2: Register with MCP Server
Update src/index.ts:
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
# 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:
-
Specification Review:
- Endpoint: GET /user/overview
- No input parameters (uses auth token)
- Returns: hashrate, rewards, worker counts
- Cache TTL: 30s
-
Schema Creation:
// 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(),
});
-
Handler Implementation: Following template with TTL=30
-
Tests: 12 tests covering validation, caching, errors
-
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:
- Cache Key includes filter hash:
const filtersHash = hashObject({ status, search, sortBy });
const cacheKey = `braiins:workers:list:${page}:${filtersHash}`;
- Pagination in response:
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
anytypes)
Common Pitfalls
Pitfall 1: Forgetting to sanitize cache keys
// 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
// 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
// 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:
- Check tool is exported from
src/tools/index.ts - Verify tool is registered in
src/index.ts - Restart MCP server after changes
Issue: Tests failing with "Cannot find module" Solution:
- Check import paths are correct
- Run
npm run buildto compile TypeScript - Verify file exists at expected path
Issue: Cache always misses Solution:
- Verify Redis connection in logs
- Check cache key generation is consistent
- Confirm TTL is set correctly
Version History
- 1.0.0 (2025-12-18): Initial skill definition for braiins-pool-mcp-server
References
- ARCHITECTURE.md - System architecture patterns
- API.md - Braiins API specification
- MCP Protocol Spec - MCP standard
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!