
mcp-tool-builder
by Ryno-Crypto-Mining-Services
An MCP server for querying data from the Braiins Pool API
SKILL.md
name: mcp-tool-builder version: 1.0.0 category: mcp-development complexity: moderate status: active created: 2025-12-18 author: braiins-pool-mcp-server
description: | 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.
triggers:
- "create MCP tool"
- "implement tool"
- "build tool for"
- "new MCP handler"
- "add tool to MCP server"
dependencies:
- mcp-schema-designer
- braiins-api-mapper
- braiins-cache-strategist
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
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です

