Agent skill
mcp-server-dev
Braiins OS MCP Server Development - Building MCP tools, resources, and prompts for Bitcoin mining operations management
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/mcp-server-dev-ryno-crypto-mining-s-braiins-os-mcp-serve
SKILL.md
Braiins OS MCP Server Development
Description
Comprehensive skill for building high-quality MCP (Model Context Protocol) tools, resources, and prompts specifically for Braiins OS miner management. This skill extends the generic mcp-builder patterns with mining-specific workflows, gRPC integration, and fleet operations optimization.
Target Audience: Developers building the braiins-os-mcp-server
When to Use This Skill
Use this skill when:
- Designing new MCP tools for miner management (status queries, firmware updates, pool configuration)
- Creating MCP resources for fleet monitoring (aggregated metrics, miner status feeds)
- Building MCP prompts for guided mining operations (troubleshooting, batch updates)
- Implementing gRPC client patterns for miner communication
- Designing Redis caching strategies for fleet-scale operations
- Optimizing MCP responses for AI agent consumption
Don't use this skill for:
- General MCP server development (use mcp-builder skill instead)
- Braiins OS API reference (use braiins-os skill for documentation)
- Infrastructure deployment (use devops-related skills)
- Non-MCP related TypeScript development
Prerequisites
Knowledge Requirements
- MCP protocol fundamentals (tools, resources, prompts, transport)
- TypeScript/Node.js development
- gRPC concepts (clients, streams, error handling)
- Redis caching patterns
- Bitcoin mining operations basics
Project Context
- Codebase: braiins-os-mcp-server (TypeScript, Node.js 20.x)
- MCP SDK: @modelcontextprotocol/sdk
- gRPC: @grpc/grpc-js
- Cache: Redis 7.x
- Testing: Jest + Supertest
Related Skills
- braiins-os (.claude/skills/braiins-os/) - Braiins OS API documentation reference
- mcp-builder (docs/claude/skills-templates/mcp-builder/) - Generic MCP development guide
- CLAUDE.md (./CLAUDE.md) - Project-specific development patterns
Workflow
Phase 1: Tool Design (Mining Operations Focus)
Step 1.1: Identify Mining Workflow
Common Mining Operations:
- Miner Status - Get current status of one or multiple miners
- Firmware Update - Update firmware with progress tracking
- Pool Configuration - Manage mining pool settings
- Fleet Metrics - Aggregated statistics across all miners
- Troubleshooting - Guided diagnostics for offline miners
Design Principle: Consolidate workflows, not just API endpoints.
Example: Firmware Update Tool Design
/**
* ❌ BAD: Granular tools forcing agent orchestration
*/
@tool({ name: "check_firmware_version" })
async checkFirmwareVersion(minerId: string) { /* ... */ }
@tool({ name: "download_firmware" })
async downloadFirmware(url: string) { /* ... */ }
@tool({ name: "flash_firmware" })
async flashFirmware(minerId: string) { /* ... */ }
/**
* ✅ GOOD: Consolidated workflow tool
*/
@tool({
name: "update_miner_firmware",
description: "Update firmware on one or more miners with automatic download, flashing, and progress tracking"
})
async updateMinerFirmware(params: {
minerIds: string[]; // Batch operation support
version: string; // Target firmware version
force?: boolean; // Skip version checks
progressCallback?: boolean; // Enable progress updates
}): Promise<{
jobId: string; // Background job ID
status: "pending" | "running" | "completed" | "failed";
progress: {
total: number;
completed: number;
failed: number;
};
}> {
// Handles: version check → download → flash → verify
// Returns job ID for async status polling
}
Step 1.2: Design Input Schema (Zod Validation)
Mining-Specific Patterns:
import { z } from "zod";
// Common validations for mining operations
const MinerIdSchema = z.string().regex(/^[a-zA-Z0-9\-_]+$/);
const MinerIdsSchema = z.array(MinerIdSchema).min(1).max(100); // Batch limit
const FirmwareVersionSchema = z.string().regex(/^\d+\.\d+\.\d+$/);
const PoolUrlSchema = z.string().url().refine(
(url) => url.startsWith("stratum+tcp://") || url.startsWith("stratum+ssl://"),
{ message: "Pool URL must use stratum protocol" }
);
// Tool input schema
const UpdateMinerFirmwareSchema = z.object({
minerIds: MinerIdsSchema,
version: FirmwareVersionSchema,
force: z.boolean().optional().default(false),
progressCallback: z.boolean().optional().default(false)
}).strict();
Step 1.3: Design Output Format (Context-Optimized)
Principle: Agents have limited context - provide concise by default, detailed on request.
// Concise output (default) - ~150 tokens
{
jobId: "update-20251228-abc123",
status: "running",
progress: {
total: 10,
completed: 7,
failed: 1,
current: "miner-008"
},
estimatedCompletion: "2025-12-28T18:30:00Z"
}
// Detailed output (detailLevel: "verbose") - ~500 tokens
{
jobId: "update-20251228-abc123",
status: "running",
progress: {
total: 10,
completed: 7,
failed: 1,
current: "miner-008",
breakdown: [
{ minerId: "miner-001", status: "completed", duration: "5m32s" },
{ minerId: "miner-002", status: "completed", duration: "5m28s" },
// ... all 10 miners
]
},
startedAt: "2025-12-28T18:00:00Z",
estimatedCompletion: "2025-12-28T18:30:00Z",
errors: [
{
minerId: "miner-005",
error: "Connection timeout",
suggestion: "Check network connectivity with ping_miner tool"
}
]
}
Phase 2: Implementation (TypeScript + MCP SDK)
Step 2.1: Tool Registration
// src/mcp/tools/updateMinerFirmware.ts
import { tool } from "@modelcontextprotocol/sdk";
import { z } from "zod";
const UpdateMinerFirmwareSchema = z.object({
minerIds: z.array(z.string()).min(1).max(100),
version: z.string().regex(/^\d+\.\d+\.\d+$/),
force: z.boolean().optional().default(false),
detailLevel: z.enum(["concise", "verbose"]).optional().default("concise")
}).strict();
@tool({
name: "update_miner_firmware",
description: "Update firmware on one or more Braiins OS miners. Handles download, flashing, and verification. Returns job ID for progress tracking.",
inputSchema: UpdateMinerFirmwareSchema,
annotations: {
readOnlyHint: false, // Modifies miner state
destructiveHint: false, // Can be rolled back
idempotentHint: true, // Safe to retry
openWorldHint: true // Interacts with external miners
}
})
export async function updateMinerFirmware(
params: z.infer<typeof UpdateMinerFirmwareSchema>
): Promise<FirmwareUpdateJobStatus> {
// Implementation in next step
}
Step 2.2: gRPC Integration Pattern
import { GrpcConnectionPool } from "../../api/grpc/pool";
import { withRetry } from "../../api/grpc/retry";
export async function updateMinerFirmware(
params: z.infer<typeof UpdateMinerFirmwareSchema>
): Promise<FirmwareUpdateJobStatus> {
const { minerIds, version, force, detailLevel } = params;
// 1. Create background job
const jobId = await this.jobQueue.createJob({
type: "firmware_update",
minerIds,
version
});
// 2. Execute updates asynchronously
this.executeInBackground(async () => {
const results = await Promise.allSettled(
minerIds.map(async (minerId) => {
// Get gRPC connection from pool
const client = await this.grpc.pool.getConnection(minerId);
// Update with retry logic
return await withRetry(
() => client.updateFirmware({ version, force }),
{ maxRetries: 3, initialDelay: 5000 }
);
})
);
// 3. Update job status
await this.jobQueue.updateJob(jobId, {
status: "completed",
results: results.map((r, i) => ({
minerId: minerIds[i],
success: r.status === "fulfilled",
error: r.status === "rejected" ? r.reason.message : undefined
}))
});
});
// 4. Return immediate response
return {
jobId,
status: "pending",
progress: { total: minerIds.length, completed: 0, failed: 0 }
};
}
Step 2.3: Redis Caching Integration
Pattern: Invalidate cached data when miners update
async function updateMinerFirmware(params: /* ... */): Promise</* ... */> {
// ... perform update ...
// Invalidate all caches related to updated miners
await Promise.all(
params.minerIds.map(async (minerId) => {
await this.redis.del(`cache:miner:${minerId}:status`);
await this.redis.del(`cache:miner:${minerId}:config`);
})
);
// Invalidate fleet-level caches
await this.redis.del("cache:fleet:summary");
// Publish update event for real-time subscribers
await this.redis.publish("events:firmware-update", JSON.stringify({
minerIds: params.minerIds,
version: params.version,
timestamp: new Date().toISOString()
}));
return { /* ... */ };
}
Phase 3: Resource Development (Fleet Monitoring)
Step 3.1: Design Resource URI Scheme
URI Pattern: braiins:///<category>/<resource>[/<identifier>]
braiins:///fleet/summary # Aggregated fleet metrics
braiins:///fleet/miners # List of all miners
braiins:///miner/miner-123/status # Single miner status
braiins:///miner/miner-123/logs # Miner logs
braiins:///jobs/update-abc123 # Job status
Step 3.2: Implement Cached Resource
import { resource } from "@modelcontextprotocol/sdk";
@resource({
uri: "braiins:///fleet/summary",
name: "Fleet Summary",
description: "Aggregated metrics for all managed miners (cached for 30s)",
mimeType: "application/json"
})
export async function getFleetSummary(): Promise<FleetSummary> {
const cacheKey = "cache:fleet:summary";
const ttl = 30; // 30 seconds - fleet data changes slowly
// Check cache
const cached = await this.redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Compute fresh data (expensive operation)
const [total, online, hashrate, temp, alerts] = await Promise.all([
this.db.countMiners(),
this.db.countMinersOnline(),
this.aggregateHashrate(), // Sum across all miners
this.averageTemperature(), // Average temp
this.getActiveAlerts() // Count critical alerts
]);
const summary: FleetSummary = {
totalMiners: total,
onlineMiners: online,
offlineMiners: total - online,
totalHashrate: `${hashrate} TH/s`,
averageTemp: `${temp}°C`,
activeAlerts: alerts.length,
lastUpdated: new Date().toISOString()
};
// Cache for 30 seconds
await this.redis.setex(cacheKey, ttl, JSON.stringify(summary));
return summary;
}
Phase 4: Prompt Development (Guided Workflows)
Step 4.1: Troubleshooting Prompt
import { prompt } from "@modelcontextprotocol/sdk";
@prompt({
name: "troubleshoot_miner_offline",
description: "Step-by-step guide to diagnose and fix offline miners",
arguments: [
{ name: "minerId", description: "Miner ID to troubleshoot", required: true }
]
})
export async function troubleshootMinerOffline(
minerId: string
): Promise<PromptMessage[]> {
// Get miner details
const miner = await this.db.getMiner(minerId);
return [
{
role: "user",
content: {
type: "text",
text: `My miner ${minerId} is offline. Can you help me fix it?`
}
},
{
role: "assistant",
content: {
type: "text",
text: `I'll help troubleshoot miner ${minerId}. Let me run through diagnostics:
**Step 1: Check Last Known Status**
Last seen: ${miner.lastSeen || "Never"}
Last status: ${miner.lastStatus || "Unknown"}
Last IP: ${miner.lastIp || "Unknown"}
**Step 2: Test Network Connectivity**
I'll ping the miner to check if it's reachable.
→ Using tool: ping_miner({ minerId: "${minerId}" })
Please wait while I test connectivity...
**Next Steps Based on Results:**
✅ If ping succeeds:
- Use tool: get_miner_logs({ minerId: "${minerId}", lines: 50 })
- Check for error patterns in logs
- May need to restart miner services
❌ If ping fails:
- Check physical power connection
- Verify network cable connected
- Check router/switch port status
- Verify IP address hasn't changed
Would you like me to proceed with the ping test?`
}
}
];
}
Phase 5: Testing (MCP-Specific)
Step 5.1: Unit Test for MCP Tool
// tests/unit/mcp/tools/updateMinerFirmware.test.ts
import { MCPServer } from "../../../../src/server";
import { createMockGrpcClient } from "../../../mocks/grpc";
import { createMockRedis } from "../../../mocks/redis";
describe("update_miner_firmware tool", () => {
let mcpServer: MCPServer;
let mockGrpc: jest.Mocked<GrpcClient>;
let mockRedis: jest.Mocked<Redis>;
beforeEach(() => {
mockGrpc = createMockGrpcClient();
mockRedis = createMockRedis();
mcpServer = new MCPServer({ grpc: mockGrpc, redis: mockRedis });
});
it("should accept valid firmware update request", async () => {
const result = await mcpServer.callTool("update_miner_firmware", {
minerIds: ["miner-1", "miner-2"],
version: "2.0.1"
});
expect(result.jobId).toBeDefined();
expect(result.status).toBe("pending");
expect(result.progress.total).toBe(2);
});
it("should reject invalid version format", async () => {
await expect(
mcpServer.callTool("update_miner_firmware", {
minerIds: ["miner-1"],
version: "invalid-version"
})
).rejects.toThrow("version must match format");
});
it("should enforce batch size limit", async () => {
const tooManyMiners = Array.from({ length: 101 }, (_, i) => `miner-${i}`);
await expect(
mcpServer.callTool("update_miner_firmware", {
minerIds: tooManyMiners,
version: "2.0.1"
})
).rejects.toThrow("maximum 100 miners");
});
});
Step 5.2: Integration Test for Resource
// tests/integration/mcp/resources/fleetSummary.test.ts
describe("Fleet Summary Resource", () => {
let mcpServer: MCPServer;
let redis: Redis;
beforeAll(async () => {
redis = new Redis(process.env.REDIS_URL);
mcpServer = new MCPServer({ redis });
});
it("should return cached fleet summary on second request", async () => {
const result1 = await mcpServer.readResource("braiins:///fleet/summary");
const result2 = await mcpServer.readResource("braiins:///fleet/summary");
expect(result1).toEqual(result2);
// Verify cache was used (mock/spy on Redis get method)
expect(redis.get).toHaveBeenCalledWith("cache:fleet:summary");
});
it("should refresh cache after TTL expiry", async () => {
await mcpServer.readResource("braiins:///fleet/summary");
// Wait for cache to expire (30s TTL)
await sleep(31000);
const result = await mcpServer.readResource("braiins:///fleet/summary");
expect(result.lastUpdated).not.toBe(/* previous timestamp */);
});
});
Mining-Specific Patterns
Pattern 1: Batch Operations with Progress Tracking
Use Case: Update firmware on 50 miners simultaneously
@tool({ name: "update_multiple_miners" })
async updateMultipleMiners(params: {
minerIds: string[];
operation: "firmware_update" | "pool_change" | "reboot";
config: any;
}) {
// Create job for background execution
const jobId = uuid();
// Process in parallel with concurrency limit
const concurrency = 10; // Max 10 simultaneous updates
const queue = new PQueue({ concurrency });
const promises = params.minerIds.map((minerId) =>
queue.add(async () => {
try {
await this.performOperation(minerId, params.operation, params.config);
await this.updateJobProgress(jobId, { completed: minerId });
} catch (error) {
await this.updateJobProgress(jobId, { failed: minerId, error });
}
})
);
// Don't wait - return job ID immediately
Promise.all(promises).then(() => {
this.updateJobStatus(jobId, "completed");
});
return {
jobId,
status: "running",
total: params.minerIds.length,
pollUrl: `braiins:///jobs/${jobId}`
};
}
Pattern 2: Real-Time Status Streaming
Use Case: Subscribe to miner status updates via Redis pub/sub
@tool({ name: "subscribe_miner_status" })
async subscribeMinerStatus(params: { minerId: string }) {
// Start gRPC stream
const stream = await this.grpc.streamMinerStatus(params.minerId);
// Publish to Redis for agent consumption
for await (const status of stream) {
await this.redis.publish(
`miner:${params.minerId}:status`,
JSON.stringify(status)
);
}
return {
subscribed: true,
channel: `miner:${params.minerId}:status`,
message: "Status updates will be published to Redis pub/sub channel"
};
}
// Agent can then read from resource:
@resource({ uri: "braiins:///miner/{minerId}/status/stream" })
async getMinerStatusStream(minerId: string) {
// Subscribe to Redis channel and return latest status
const status = await this.redis.get(`miner:${minerId}:status:latest`);
return JSON.parse(status);
}
Pattern 3: Actionable Error Guidance
Use Case: Miner unreachable - guide agent to next steps
async function pingMiner(minerId: string) {
try {
const client = await this.grpc.pool.getConnection(minerId);
await client.ping({ timeout: 5000 });
return { reachable: true, latency: "45ms" };
} catch (error) {
// Return actionable error with next steps
return {
reachable: false,
error: {
code: "MINER_UNREACHABLE",
message: `Cannot connect to miner ${minerId}`,
suggestions: [
"Check if miner is powered on",
"Verify network connectivity with: list_miners",
"Check miner IP address in configuration",
"Try rebooting miner with: reboot_miner"
],
possibleCauses: [
"Miner offline or powered off",
"Network firewall blocking gRPC port (50051)",
"Incorrect IP address in database",
"Miner experiencing hardware failure"
]
}
};
}
}
Quality Standards
MCP Tool Checklist
- Input Validation: Zod schema with proper constraints
- Batch Support: Handles multiple miners when applicable
- Concise Output: Default response < 300 tokens
- Detailed Option: Verbose mode available via
detailLevelparam - Error Guidance: Errors include suggestions for next steps
- Caching: Reads use cached data when appropriate
- Cache Invalidation: Writes invalidate related caches
- Background Jobs: Long operations return job ID immediately
- Progress Tracking: Job progress available via separate tool/resource
- Annotations: readOnlyHint, destructiveHint, idempotentHint, openWorldHint set correctly
- Documentation: Clear description with examples
- Tests: Unit tests cover happy path + error cases
MCP Resource Checklist
- URI Format: Follows
braiins:///<category>/<resource>pattern - Caching: Appropriate TTL for data type
- Freshness: lastUpdated timestamp included
- Mime Type: Correct content type (application/json, text/plain, etc.)
- Performance: Queries optimized for large fleets
- Tests: Integration tests verify caching behavior
MCP Prompt Checklist
- Guided Workflow: Clear step-by-step instructions
- Tool References: Suggests specific tools for each step
- Resource References: Links to relevant resources
- Conditional Logic: Different paths based on outcomes
- Next Steps: Always provides clear next actions
- Context-Aware: Uses miner-specific data in guidance
Common Pitfalls
❌ Pitfall 1: Not Supporting Batch Operations
Problem: Tool only accepts single miner ID
// BAD
@tool({ name: "get_miner_status" })
async getMinerStatus(minerId: string) { /* ... */ }
// Agent must call 50 times for 50 miners
Solution: Accept array of IDs
// GOOD
@tool({ name: "get_miner_status" })
async getMinerStatus(minerIds: string[]) {
return Promise.all(minerIds.map(id => this.fetchStatus(id)));
}
// Agent calls once for all 50 miners
❌ Pitfall 2: Returning Too Much Data
Problem: Tool returns 5000+ token response overwhelming context
// BAD - Returns full miner details
{
minerId: "miner-123",
status: "running",
hashrate: { /* 20 fields */ },
temperature: { /* 15 fields */ },
fans: [ /* 6 fans × 10 fields each */ ],
pools: [ /* 3 pools × 30 fields each */ ],
// ... 4500 more tokens
}
Solution: Concise by default, detailed on request
// GOOD - Concise default
{
minerId: "miner-123",
status: "running",
hashrate: "95 TH/s",
temp: "65°C",
issues: [] // Only show if problems exist
}
// Detailed available via detailLevel: "verbose"
❌ Pitfall 3: Blocking on Long Operations
Problem: Firmware update takes 10 minutes, blocking agent context
// BAD
@tool({ name: "update_firmware" })
async updateFirmware(params: {/* ... */}) {
// Wait for 10 minute update to complete
await this.performUpdate(params);
return { status: "completed" };
}
// Agent stuck for 10 minutes
Solution: Return job ID immediately, poll separately
// GOOD
@tool({ name: "update_firmware" })
async updateFirmware(params: {/* ... */}) {
const jobId = await this.startBackgroundUpdate(params);
return {
jobId,
status: "pending",
pollWith: "check_job_status"
};
}
// Separate tool for polling
@tool({ name: "check_job_status" })
async checkJobStatus(jobId: string) {
return await this.jobQueue.getStatus(jobId);
}
Integration Notes
Related Skills
- braiins-os - Braiins OS API documentation and reference
- mcp-builder - Generic MCP server development patterns
- grpc-client-dev (planned) - gRPC client implementation patterns
- redis-caching-patterns (planned) - Advanced Redis caching strategies
Related Commands
/start-session- Initialize development session with project context/close-session- End session with documentation updates/test-all- Run comprehensive test suite/test-mcp-tools(planned) - Test MCP tools in isolation
Related Agents
- Architect - System design and API design review
- Builder - Feature implementation
- Validator - Testing and code review
- Scribe - Documentation updates
Version History
- 1.0.0 (2025-12-28): Initial skill creation
- Mining-specific MCP development patterns
- Tool/Resource/Prompt workflows
- gRPC integration patterns
- Redis caching strategies
- Batch operations and progress tracking
- Quality checklists and common pitfalls
Metadata
{
"name": "mcp-server-dev",
"version": "1.0.0",
"description": "Braiins OS MCP Server Development Skill",
"author": "braiins-os-mcp-server project",
"created": "2025-12-28",
"status": "active",
"complexity": "complex",
"category": "mcp-development",
"tags": ["mcp", "braiins-os", "mining", "grpc", "redis", "typescript"],
"extends": "mcp-builder",
"related_skills": ["braiins-os", "mcp-builder"],
"target_project": "braiins-os-mcp-server"
}
Didn't find tool you were looking for?