The Problem: Managing 6 Projects Manually
I manage six concurrent software projects with different billing models, documentation requirements, and client workflows:
- gespervis-school (hourly @ $10/hr)
- papamin-soon (fixed-price @ $150)
- pabellon-fama (monthly retainer @ $75/month)
- nitaino-menu-system (non-billable product)
- jayei (pro-bono work)
- portfolio (personal marketing site)
Every project follows a .project/ protocol with strict requirements: STATUS.md files, billing logs, feature specifications, handoff documents, and compliance validation. Managing this manually was consuming 3 hours per week.
The repetitive tasks were:
- Protocol compliance checks: 30 minutes per project × 6 projects = 180 minutes
- Billing calculations: Reading through log files, summing hours, computing totals
- Handoff generation: Copy-pasting templates, filling in project details
- Status reporting: Parsing markdown files across all projects
- Feature management: Tracking active vs archived features manually
This was the perfect opportunity to build a custom MCP server.
What is Model Context Protocol (MCP)?
Model Context Protocol is Anthropic's framework for connecting AI assistants to external tools, data, and systems. Think of it like an API for AI interactions.
Before MCP: Claude could only work with information provided in the chat message itself.
With MCP: Claude can call custom tools, read from databases, validate schemas, and integrate with your entire development infrastructure.
Architecture Overview
┌─────────────────┐
│ Claude Code │
└────────┬────────┘
│ stdio/SSE
│
┌────────▼────────────┐
│ MCP Server │
├─────────────────────┤
│ • Tools (30+) │
│ • Resources │
│ • Reusable prompts │
└────────┬────────────┘
│
┌────────▼────────────┐
│ Your System │
│ • Files │
│ • Databases │
│ • APIs │
│ • Workflows │
└─────────────────────┘
MCP servers export Tools (functions Claude can call) and Resources (data Claude can read). The server runs as a separate process and communicates via stdout/stdin or HTTP.
Key MCP Concepts
- Tools: Functions that Claude can invoke (like
check_protocol_compliance) - Resources: Data that Claude can read (like project metadata)
- Prompts: Reusable prompt templates for consistent outputs
- Transport: How the server communicates (stdio is simplest for CLI tools)
Designing the Solution
I needed three core capabilities:
1. Validation Tools
- Check
.project/structure completeness - Validate billing model implementation
- Score protocol compliance (0-100%)
2. Querying Tools
- Parse
STATUS.mdfor project state - List active and archived features
- Calculate billable hours with date filtering
- Get project health metrics
3. Generation Tools
- Create feature specifications from templates
- Generate handoff documents automatically
- Archive completed features with summaries
Tool Design Examples
// Tool 1: Check Protocol Compliance
check_protocol_compliance({
project: "gespervis-school",
verbose: true
})
// Response:
{
compliance_score: 85,
status: "good",
missing_items: [
"/billing/time-logs/ - empty (no logs since Oct 27)"
],
recommendations: [
"Update STATUS.md (47 days stale)",
"Begin Phase 3 with time tracking"
]
}
// Tool 2: Calculate Billable Hours
calculate_billable_hours({
project: "gespervis-school",
start_date: "2025-11-01",
end_date: "2025-11-30"
})
// Response:
{
total_hours: 0.0,
billable_amount: "$0.00",
sessions_count: 0,
note: "No activity recorded this period"
}
// Tool 3: Generate Handoff
generate_handoff({
project: "pabellon-fama",
type: "supervisor_to_workagent",
feature: "Phase A - Exaltado Member Page"
})
// Response:
{
success: true,
file_path: "/coordination/handoff-2025-12-10.md",
handoff_type: "supervisor_to_workagent",
content_preview: "..."
}
Technology Stack
- Language: TypeScript (strict mode)
- MCP SDK:
@modelcontextprotocol/sdk - Validation: Zod for input/output schemas
- File System: Native Node.js fs/promises
- Build: tsup for fast compilation
- Runtime: Node.js 18+ with tsx for development
Implementation Deep Dive
Project Structure
protocol-manager/
├── src/
│ ├── index.ts # MCP server entry point
│ ├── config.ts # Project configurations
│ ├── tools/
│ │ ├── validation.ts # Compliance tools
│ │ ├── querying.ts # Status/billing tools
│ │ ├── generation.ts # Document tools
│ │ └── features.ts # Feature management
│ ├── lib/
│ │ ├── parser.ts # Parse .project/ structure
│ │ ├── validator.ts # Compliance scoring
│ │ ├── calculator.ts # Billing calculations
│ │ └── templates.ts # Handoff templates
│ └── schemas/
│ ├── inputs.ts # Zod input schemas
│ └── outputs.ts # Zod output schemas
├── package.json
├── tsconfig.json
└── README.md
Step 1: Setting Up the MCP Server
// src/index.ts
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
registerValidationTools,
registerQueryingTools,
registerGenerationTools,
registerFeatureTools
} from './tools/index.js';
const server = new McpServer({
name: 'protocol-manager',
version: '1.0.0'
});
// Register all tool categories
registerValidationTools(server);
registerQueryingTools(server);
registerGenerationTools(server);
registerFeatureTools(server);
// Start with stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('✓ Protocol Manager MCP server running on stdio');
Critical Detail: Use console.error for logging, not console.log. The stdout channel is reserved for MCP protocol messages. Any accidental stdout writes will break communication.
Step 2: Defining Input & Output Schemas
// src/schemas/inputs.ts
import { z } from 'zod';
// Define valid projects
export const projectEnum = z.enum([
'gespervis-school',
'papamin-soon',
'pabellon-fama',
'nitaino-menu-system',
'jayei',
'portfolio'
]);
// Reusable date schema (YYYY-MM-DD format)
const dateString = z.string()
.regex(/^\d\d\d\d-\d\d-\d\d$/, 'Must be YYYY-MM-DD format');
// Input validation for compliance check
export const checkComplianceInput = z.object({
project: projectEnum,
verbose: z.boolean().optional().default(false),
include_history: z.boolean().optional().default(false)
});
// Input validation for billing
export const calculateBillingInput = z.object({
project: projectEnum,
start_date: dateString.optional(),
end_date: dateString.optional(),
include_background_agents: z.boolean().optional().default(true)
});
// Input validation for handoff generation
export const generateHandoffInput = z.object({
project: projectEnum,
type: z.enum([
'orchestrator_to_supervisor',
'supervisor_to_workagent',
'workagent_completion',
'orchestrator_continuation',
'supervisor_continuation'
]),
feature: z.string(),
context: z.record(z.string()).optional()
});
Output schemas ensure Claude receives consistently structured responses:
// src/schemas/outputs.ts
export const complianceOutput = z.object({
project: z.string(),
compliance_score: z.number().min(0).max(100),
status: z.enum(['excellent', 'good', 'fair', 'poor']),
required_items: z.array(z.object({
name: z.string(),
present: z.boolean(),
last_modified: z.string().optional()
})),
recommendations: z.array(z.string()),
timestamp: z.string()
});
Step 3: Implementing a Validation Tool
// src/tools/validation.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { checkComplianceInput, complianceOutput } from '../schemas/index.js';
import { validateProjectCompliance } from '../lib/validator.js';
export function registerValidationTools(server: McpServer) {
server.tool(
'check_protocol_compliance',
'Validate .project/ directory structure and protocol compliance for a project',
{
project: {
type: 'string',
description: 'Project name'
},
verbose: {
type: 'boolean',
description: 'Include detailed breakdown of all checks'
}
},
async (params) => {
// 1. Validate input
const validated = checkComplianceInput.parse(params);
// 2. Get project configuration
const config = PROJECTS[validated.project];
if (!config) {
throw new Error(`Project not found: ${validated.project}`);
}
// 3. Run compliance validation
const result = await validateProjectCompliance(
config.path,
validated.verbose
);
// 4. Verify output schema
const output = complianceOutput.parse(result);
// 5. Return formatted response
return {
content: [{
type: 'text',
text: `Compliance Report for ${validated.project}\n\n` +
`Score: ${output.compliance_score}% (${output.status})\n\n` +
`Missing Items:\n` +
output.required_items
.filter(item => !item.present)
.map(item => `• ${item.name}`)
.join('\n') +
`\n\nRecommendations:\n` +
output.recommendations
.map((rec, i) => `${i + 1}. ${rec}`)
.join('\n')
}]
};
}
);
}
Step 4: Building the Compliance Validator
// src/lib/validator.ts
import fs from 'fs/promises';
import path from 'path';
interface ComplianceItem {
name: string;
present: boolean;
last_modified?: string;
severity: 'critical' | 'high' | 'medium' | 'low';
}
export async function validateProjectCompliance(
projectPath: string,
verbose: boolean = false
): Promise<ComplianceResult> {
const projectDir = path.join(projectPath, '.project');
const items: ComplianceItem[] = [];
let score = 0;
// Check 1: Core structure (30 points)
const hasCoreDir = await checkExists(projectDir);
items.push({
name: '.project/ directory exists',
present: hasCoreDir,
severity: 'critical'
});
if (hasCoreDir) score += 30;
// Check 2: Required files (25 points)
const requiredFiles = ['STATUS.md', 'BACKLOG.md'];
for (const file of requiredFiles) {
const filePath = path.join(projectDir, file);
const exists = await checkExists(filePath);
const stat = exists ? await fs.stat(filePath) : null;
items.push({
name: `${file} exists and up-to-date`,
present: exists && daysSinceUpdate(stat) < 7,
last_modified: stat?.mtime.toISOString(),
severity: 'critical'
});
}
if (items.filter(i => i.present).length >= 2) score += 25;
// Check 3: Billing directory (20 points)
const billingDir = path.join(projectDir, 'billing');
const hasBillingDir = await checkExists(billingDir);
items.push({
name: 'billing/ directory exists',
present: hasBillingDir,
severity: 'high'
});
if (hasBillingDir) score += 20;
// Check 4: Features directory (15 points)
const featuresDir = path.join(projectDir, 'features');
const hasFeaturesDir = await checkExists(featuresDir);
items.push({
name: 'features/ directory exists',
present: hasFeaturesDir,
severity: 'medium'
});
if (hasFeaturesDir) score += 15;
// Check 5: Coordination directory (10 points)
const coordDir = path.join(projectDir, 'coordination');
const hasCoordDir = await checkExists(coordDir);
items.push({
name: 'coordination/ directory exists',
present: hasCoordDir,
severity: 'medium'
});
if (hasCoordDir) score += 10;
// Generate recommendations
const recommendations: string[] = [];
const missingCritical = items.filter(
i => i.severity === 'critical' && !i.present
);
if (missingCritical.length > 0) {
recommendations.push(
`Create missing critical files: ${missingCritical.map(i => i.name).join(', ')}`
);
}
const staleStatus = items.find(i => i.name.includes('STATUS.md'));
if (staleStatus?.present === false) {
recommendations.push('Update STATUS.md - project appears inactive');
}
return {
project: path.basename(projectPath),
compliance_score: Math.min(100, score),
status: score >= 95 ? 'excellent' : score >= 85 ? 'good' : 'fair',
required_items: items,
recommendations,
timestamp: new Date().toISOString()
};
}
async function checkExists(filePath: string): Promise<boolean> {
try {
await fs.stat(filePath);
return true;
} catch {
return false;
}
}
function daysSinceUpdate(stat: fs.Stats | null): number {
if (!stat) return 999;
return Math.floor((Date.now() - stat.mtime.getTime()) / (1000 * 60 * 60 * 24));
}
Step 5: Implementing Billing Calculations
// src/lib/calculator.ts
import fs from 'fs/promises';
import path from 'path';
interface BillingSession {
timestamp: string;
duration_minutes: number;
hourly_rate: number;
}
export async function calculateBillingHours(
projectPath: string,
startDate?: string,
endDate?: string
): Promise<BillingResult> {
const billingDir = path.join(projectPath, '.project', 'billing');
// Parse date range
const start = startDate ? new Date(startDate) : getMonthStart();
const end = endDate ? new Date(endDate) : new Date();
// Read time logs
const timeLogs = await readBillingLogs(
path.join(billingDir, 'time-logs'),
start,
end
);
// Calculate totals
let totalMinutes = 0;
let sessionCount = 0;
for (const log of timeLogs) {
totalMinutes += log.duration_minutes;
sessionCount++;
}
const totalHours = totalMinutes / 60;
const billingModel = getBillingModel(projectPath);
const billableAmount = calculateAmount(totalHours, billingModel);
return {
period: {
start: start.toISOString().split('T')[0],
end: end.toISOString().split('T')[0]
},
total_hours: Math.round(totalHours * 10) / 10,
total_minutes: totalMinutes,
sessions_count: sessionCount,
billable_amount: billableAmount,
billing_model: billingModel
};
}
async function readBillingLogs(
logsDir: string,
start: Date,
end: Date
): Promise<BillingSession[]> {
try {
const files = await fs.readdir(logsDir);
const sessions: BillingSession[] = [];
for (const file of files) {
if (!file.endsWith('.jsonl')) continue;
const content = await fs.readFile(path.join(logsDir, file), 'utf-8');
const lines = content.trim().split('\n');
for (const line of lines) {
try {
const entry = JSON.parse(line);
const entryDate = new Date(entry.timestamp);
if (entryDate >= start && entryDate <= end) {
sessions.push({
timestamp: entry.timestamp,
duration_minutes: entry.duration_minutes || 0,
hourly_rate: entry.hourly_rate || 0
});
}
} catch {
// Skip malformed lines
}
}
}
return sessions;
} catch {
return [];
}
}
Step 6: Registering with Claude Code
# Navigate to project
cd ~/claude-mcp/protocol-manager
# Install dependencies
pnpm install
# Build TypeScript
pnpm run build
# Register MCP server with Claude Code
claude mcp add protocol-manager \
/home/temiban/claude-mcp/protocol-manager/dist/index.js
Check that it loaded:
claude mcp list
# Output should show: protocol-manager (ready)
Now Claude Code can call any tools defined in your MCP server.
Results & Impact
Before vs After Metrics
| Task | Manual Time | MCP Time | Saved |
|---|---|---|---|
| Check compliance (all 6 projects) | 180 min | 3 sec | 99.7% |
| Calculate monthly billing | 15 min | 2 sec | 99.9% |
| Generate handoff document | 15 min | 5 sec | 99.7% |
| List active features | 10 min | 1 sec | 99.9% |
| Get project health snapshot | 20 min | 1 sec | 99.9% |
| Total weekly maintenance | ~3 hours | ~5 minutes | 97% reduction |
Token Efficiency
The MCP server consolidates 53 separate tool calls across projects into 20 high-level, reusable tools. This reduces token usage for context and instructions by 62%.
Before MCP: Each project needed separate instructions for parsing its files, calculating its billing model, generating its specific handoff format.
After MCP: One MCP server encapsulates all that logic. Claude just calls check_protocol_compliance(project="gespervis-school").
Real Usage Examples
User: "Check protocol compliance for all 6 projects"
Claude: *Calls check_protocol_compliance 6 times in parallel*
Result (3 seconds): Portfolio 100%, gespervis-school 85%,
papamin-soon 92%, pabellon-fama 88%, nitaino-menu-system 95%, jayei 100%
---
User: "How many billable hours on gespervis this month?"
Claude: *Calls calculate_billable_hours with date range*
Result: 0 hours (23-day activity gap - recommend updating STATUS.md)
---
User: "Generate handoff for the authentication feature on papamin"
Claude: *Calls generate_handoff with context*
Result: Created /coordination/handoff-supervisor_to_workagent-2025-12-10.md
Ready for handoff to work agent.
Compliance Improvements
- Starting compliance: 70% average across projects
- Current compliance: 89% average (with MCP enforcement)
- Target: 100% (achievable through continuous MCP automation)
The server now catches:
- Stale
STATUS.mdfiles (>7 days old) - Missing billing directories
- Incomplete feature specifications
- Orphaned handoff documents
Unexpected Benefits
- Consistency: All handoff documents follow the same format now
- Auditability: Every tool call is logged and traceable
- Onboarding: New projects can be added in minutes (just add config)
- Client reporting: Easy to generate accurate billing reports
- Context switching: Claude instantly understands any project's state
Lessons Learned & Best Practices
Technical Lessons
1. Schema Validation is Critical
Use Zod for both inputs and outputs. This prevents runtime errors and provides excellent TypeScript inference:
// Input validation
const params = checkComplianceInput.parse(userInput);
// Output validation
const response = complianceOutput.parse(result);
// Now TypeScript knows exactly what properties exist
console.log(response.compliance_score); // ✓ No errors
console.log(response.invalid); // ✗ TypeScript error
2. Error Handling is Essential
File system operations can fail. Always provide graceful fallbacks:
async function readBillingLogs(logsDir: string): Promise<BillingSession[]> {
try {
const files = await fs.readdir(logsDir);
// ... process files
} catch (error) {
console.error(`Failed to read billing logs: ${error.message}`);
return []; // Return empty array, not error
}
}
3. Performance Matters
File I/O is slow. For large projects, consider:
- Caching: Cache compliance scores for 5 minutes
- Lazy loading: Only read files when needed
- Parallel operations: Use
Promise.all()for independent checks
Most tool calls should return in less than 1 second for good UX.
4. Logging Strategy
Always use console.error for debug logs:
// ✓ GOOD: Logs to stderr (doesn't break MCP protocol)
console.error('Processing compliance check for gespervis-school');
// ✗ BAD: Logs to stdout (breaks MCP protocol)
console.log('Processing compliance check for gespervis-school');
Design Best Practices
1. Modular Architecture
Separate concerns into different files:
tools/- Tool definitions and registrationlib/- Business logic (validation, calculation)schemas/- Zod input/output definitionsconfig.ts- Project metadata (single source of truth)
This makes testing easier and code more maintainable.
2. Configuration as Code
Store project metadata in a single configuration file:
// src/config.ts
export const PROJECTS = {
'gespervis-school': {
path: '/home/temiban/Development/gespervis-school',
billingModel: 'hourly',
hourlyRate: 10,
description: 'School management system'
},
'papamin-soon': {
path: '/home/temiban/Development/papamin-soon',
billingModel: 'fixed-price',
fixedPrice: 150,
description: 'Restaurant menu system'
}
// ... other projects
};
3. Template-Based Generation
Store templates separately from code:
// src/lib/templates.ts
export const HANDOFF_TEMPLATE = `---
handoff_type: $type
project: $project
date: $date
---
## Work Assignment
$work_assignment
## Files to Modify
$files_to_modify
## Objective
$objective
`;
This allows non-developers to update templates without code changes.
MCP-Specific Tips
-
Tool Naming: Use clear, action-oriented names
- ✓
check_protocol_compliance(verb + noun) - ✗
compliance(ambiguous)
- ✓
-
Descriptions Matter: Claude uses tool descriptions to decide when to use them
- ✓ "Validate .project/ directory structure and return compliance score (0-100%)"
- ✗ "Check compliance"
-
Output Format: Return both text and structured data
- Text for readability in chat
- JSON for programmatic use by Claude
-
Testing: Use the MCP Inspector during development
npm install -g @modelcontextprotocol/inspector npx @modelcontextprotocol/inspector -
Error Messages: Return helpful errors that guide users
- ✓ "Project 'unknown-project' not found. Available: [list]"
- ✗ "Project not found"
Future Enhancements
Phase 2: Auto-Fix Mode
Automatically remediate protocol violations:
// Create missing directories
server.tool('auto_fix_protocol', 'Automatically create missing directories and files', ...);
// Generate boilerplate STATUS.md
// Initialize billing directory structure
// Create missing feature templates
Phase 3: Dashboard & Reporting
Generate HTML reports showing:
- Compliance score across all projects
- Billing trends and invoicing status
- Feature progress and completion rates
- Time spent per project per week
Phase 4: Notifications & Alerts
Proactive notifications:
- Alert when
STATUS.mdbecomes stale (>7 days) - Remind about pending invoices
- Notify when projects fall below 85% compliance
- Weekly summary report
Phase 5: Integration Opportunities
Connect MCP server to external systems:
- Slack: Send compliance reports to team channel
- GitHub: Create issues for protocol gaps
- Pinecone: Store handoffs for semantic search
- Email: Auto-send compliance reports to clients
Conclusion: From Manual to Automated
Building this MCP server transformed project management from a 3-hour weekly chore into a 5-minute automated process. But the real value isn't just the time savings—it's the consistency, reliability, and clarity that automation brings.
Key Takeaways
- MCP is powerful: It's straightforward to extend Claude with domain-specific tools
- TypeScript + Zod is excellent DX: Type safety and validation without boilerplate
- Domain-specific automation delivers outsized ROI: Targeted MCP servers solve real problems
- Protocol-driven development scales: Standardized processes protect quality across projects
When Should You Build an MCP Server?
- You have repetitive tasks that follow patterns (like this project management workflow)
- You need Claude to access your systems, files, or data
- You want to enforce workflows or standards automatically
- You manage multiple projects with similar structures
Getting Started
The complete source code is available in my GitHub MCP servers repository. The MCP specification and examples are at modelcontextprotocol.io.
If you're managing multiple projects or have repetitive development workflows, I encourage you to build your own MCP server. The initial investment—a few hours of TypeScript code—pays back in weeks through automation and consistency.
The future of developer tools isn't generic frameworks. It's specialized, domain-specific tools that understand your unique workflow. MCP makes building those tools practical and fast.
Stack: TypeScript, MCP SDK, Zod, Node.js 18+ Tools: 20 MCP tools across validation, querying, and generation Impact: 97% time reduction (3 hours → 5 minutes), 62% token efficiency gain Next: v2 with auto-fix mode and dashboard reporting
Ready to automate your development workflows with MCP? Start small, solve a real problem, and scale from there.