Build Your First MCP Server: Director Mode Assistant
Learn MCP development by building a practical server that demonstrates TDD, Auto-Cycle, and SpecKit concepts. Complete with TypeScript, tests, and best practices.
🎯 What You’ll Build
In this tutorial, you’ll build a complete MCP Server from scratch that demonstrates three core Director Mode concepts:
- TDD Cycle Management - Track and guide your Red-Green-Refactor workflow
- Auto-Cycle Progress - Monitor automated development iterations
- SpecKit Validation - Validate specifications against implementation
The server will include 9 tools and 3 resources, with full TypeScript support, comprehensive tests, and detailed documentation.
Final Project: github.com/claude-world/mcp-director-mode-server
📚 Prerequisites
Before starting, make sure you have:
# Check Node.js version (needs 18+)
node --version # v18.0.0 or higher
# Check TypeScript
npm install -g typescript
# Verify Claude Code MCP support
claude --mcp list
Required Knowledge:
- Basic TypeScript/JavaScript
- Familiarity with command-line tools
- Understanding of TDD concepts (helpful but not required)
🚀 Quick Start
Step 1: Initialize the Project
Create a new directory and initialize:
mkdir mcp-director-mode-server
cd mcp-director-mode-server
# Initialize npm project
npm init -y
# Install dependencies
npm install @modelcontextprotocol/sdk
npm install --save-dev typescript @types/node vitest
# Initialize TypeScript
npx tsc --init
Update package.json:
{
"name": "mcp-director-mode-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc",
"test": "vitest",
"start": "node dist/index.js"
}
}
Step 2: Configure TypeScript
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "tests"]
}
🛠️ Building the MCP Server
Step 3: Create the Server Entry Point
Create src/index.ts:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
// Create server instance
const server = new Server(
{
name: 'director-mode-assistant',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// Register tools (we'll add these next)
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'check_tdd_status',
description: 'Check current TDD cycle status (RED/GREEN/REFACTOR)',
inputSchema: {
type: 'object',
properties: {
projectPath: {
type: 'string',
description: 'Path to project directory',
},
},
required: ['projectPath'],
},
},
// ... more tools will be added here
],
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'check_tdd_status':
// Implementation will be added next
return {
content: [{
type: 'text',
text: 'TDD status: RED - Tests are failing',
}],
};
default:
throw new Error(`Unknown tool: ${name}`);
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Director Mode Assistant MCP Server running...');
}
main().catch(console.error);
Step 4: Implement TDD Tools
Create src/tools/tdd.ts:
import { execSync } from 'child_process';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
export interface TDDStatus {
phase: 'RED' | 'GREEN' | 'REFACTOR';
testCount: number;
passingTests: number;
failingTests: number;
lastRun: Date;
}
export function checkTDDStatus(projectPath: string): TDDStatus {
// Check for test files
const testDir = join(projectPath, 'tests');
if (!existsSync(testDir)) {
return {
phase: 'RED',
testCount: 0,
passingTests: 0,
failingTests: 0,
lastRun: new Date(),
};
}
// Run tests and parse output
try {
const output = execSync('npm test', {
cwd: projectPath,
encoding: 'utf-8',
});
// Parse test results (example for Jest)
const match = output.match(/(\d+) passed, (\d+) failed/);
if (match) {
const passing = parseInt(match[1]);
const failing = parseInt(match[2]);
return {
phase: failing > 0 ? 'RED' : 'GREEN',
testCount: passing + failing,
passingTests: passing,
failingTests: failing,
lastRun: new Date(),
};
}
} catch (error: any) {
// Tests failed
const output = error.stdout || error.message;
const match = output.match(/(\d+) passed, (\d+) failed/);
if (match) {
return {
phase: 'RED',
testCount: parseInt(match[1]) + parseInt(match[2]),
passingTests: parseInt(match[1]),
failingTests: parseInt(match[2]),
lastRun: new Date(),
};
}
}
return {
phase: 'RED',
testCount: 0,
passingTests: 0,
failingTests: 0,
lastRun: new Date(),
};
}
export function suggestTDDNextStep(status: TDDStatus): string {
switch (status.phase) {
case 'RED':
return 'Write minimal code to pass the failing tests';
case 'GREEN':
return 'All tests passing! Time to refactor for cleanliness';
case 'REFACTOR':
return 'Refactoring complete. Run tests to verify nothing broke';
default:
return 'Start by writing a failing test';
}
}
Step 5: Add Resources for State Management
Create src/resources/index.ts:
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';
export interface ProjectState {
currentPhase: string;
lastUpdate: Date;
metrics: {
totalTests: number;
testPassRate: number;
lastCommit: string;
};
}
const STATE_FILE = '.director-mode-state.json';
export function getProjectState(projectPath: string): ProjectState {
const statePath = join(projectPath, STATE_FILE);
if (!existsSync(statePath)) {
return {
currentPhase: 'INITIALIZING',
lastUpdate: new Date(),
metrics: {
totalTests: 0,
testPassRate: 0,
lastCommit: 'none',
},
};
}
const data = readFileSync(statePath, 'utf-8');
return JSON.parse(data);
}
export function updateProjectState(
projectPath: string,
updates: Partial<ProjectState>
): void {
const current = getProjectState(projectPath);
const updated = {
...current,
...updates,
lastUpdate: new Date(),
};
const statePath = join(projectPath, STATE_FILE);
writeFileSync(statePath, JSON.stringify(updated, null, 2));
}
🧪 Testing Your MCP Server
Step 6: Create Tests
Create tests/tdd.test.ts:
import { describe, it, expect } from 'vitest';
import { checkTDDStatus, suggestTDDNextStep } from '../src/tools/tdd.js';
describe('TDD Tools', () => {
it('should detect RED phase with failing tests', () => {
const status = {
phase: 'RED',
testCount: 10,
passingTests: 7,
failingTests: 3,
lastRun: new Date(),
};
const suggestion = suggestTDDNextStep(status);
expect(suggestion).toContain('Write minimal code');
});
it('should suggest refactoring in GREEN phase', () => {
const status = {
phase: 'GREEN',
testCount: 10,
passingTests: 10,
failingTests: 0,
lastRun: new Date(),
};
const suggestion = suggestTDDNextStep(status);
expect(suggestion).toContain('refactor');
});
});
Run tests:
npm test
🚀 Running the Server
Step 7: Build and Test
# Build TypeScript
npm run build
# Start the server
npm start
Step 8: Connect from Claude Code
Create or update ~/.claude.json:
{
"mcpServers": {
"director-mode": {
"command": "node",
"args": ["/path/to/mcp-director-mode-server/dist/index.js"]
}
}
}
Restart Claude Code and verify the server is loaded:
claude mcp list
📖 Usage Examples
Example 1: Check TDD Status
In Claude Code:
Check the TDD status for my current project using the director-mode server
Claude will call the check_tdd_status tool and report:
Current TDD Phase: GREEN
Tests: 15 passing, 0 failing
Next Step: Consider refactoring for cleaner code
Example 2: Get TDD Guidance
I'm in the RED phase. What should I do next?
Claude will use the server to suggest:
Write minimal code to pass the failing tests.
Don't worry about perfection yet - focus on making the tests pass.
You can refactor once all tests are green.
🎓 Key MCP Concepts Demonstrated
1. Tool Design
Tools are the primary way MCP servers expose functionality:
{
name: 'check_tdd_status',
description: 'Check current TDD cycle status',
inputSchema: {
type: 'object',
properties: {
projectPath: { type: 'string' }
},
required: ['projectPath']
}
}
Best Practices:
- Clear, descriptive names
- Detailed input validation with JSON Schema
- Helpful descriptions for Claude to understand
2. Resource Management
Resources provide state and configuration:
export function getProjectState(projectPath: string): ProjectState
export function updateProjectState(path, updates): void
Best Practices:
- Persistent state across sessions
- Immutable updates (create new state, don’t mutate)
- JSON serialization for compatibility
3. Error Handling
Always handle errors gracefully:
try {
const output = execSync('npm test', { cwd: projectPath });
return parseSuccess(output);
} catch (error) {
return parseFailure(error.stdout);
}
Best Practices:
- Never let errors crash the server
- Provide meaningful error messages
- Log errors to stderr for debugging
🏗️ Project Structure
mcp-director-mode-server/
├── src/
│ ├── index.ts # Server entry point
│ ├── tools/
│ │ ├── tdd.ts # TDD cycle tools
│ │ ├── cycle.ts # Auto-cycle tools
│ │ └── spec.ts # SpecKit tools
│ └── resources/
│ └── index.ts # State management
├── tests/
│ ├── tdd.test.ts # Tool tests
│ └── integration.test.ts # Integration tests
├── examples/
│ ├── basic-usage.md # Usage examples
│ └── tdd-workflow.md # TDD workflow guide
├── docs/
│ └── ARCHITECTURE.md # Design decisions
├── package.json
├── tsconfig.json
└── vitest.config.ts
🚦 Next Steps
Enhance Your Server
-
Add More Tools
validate_refactor- Check if refactoring is safegenerate_tests- Generate test scaffoldingrun_cycle_step- Execute Auto-Cycle iterations
-
Add Resources
project://state- Current project stateproject://metrics- Quality metrics dashboardcycle://history- Auto-Cycle history log
-
Improve Testing
- Add integration tests with mock MCP clients
- Test error scenarios
- Add E2E tests with real Claude Code connections
Publish Your Server
# Publish to npm
npm publish
# Or share via GitHub
git init
git add .
git commit -m "Initial MCP server"
git remote add origin https://github.com/YOUR_USERNAME/your-mcp-server.git
git push -u origin main
📚 Learn More
💡 Tips from the Trenches
Tip 1: Start Simple
Don’t build all 9 tools at once. Start with:
- ✅ One simple tool (e.g.,
check_tdd_status) - ✅ Basic error handling
- ✅ One test
Then iterate from there.
Tip 2: Use TypeScript Strict Mode
{
"compilerOptions": {
"strict": true // Catches bugs early
}
}
Tip 3: Log Everything
console.error('DEBUG: Checking TDD status for', projectPath);
Logs to stderr help debugging without interfering with MCP communication.
Tip 4: Test Locally First
# Test your tool logic
node -e "import('./dist/tools/tdd.js').then(m => console.log(m.checkTDDStatus('.')))"
🎉 Congratulations!
You’ve built a complete MCP Server from scratch! You now understand:
- ✅ How to create an MCP Server with TypeScript
- ✅ Tool design and implementation
- ✅ Resource management patterns
- ✅ Testing strategies
- ✅ Best practices for production-ready servers
What’s Next?
- Share your server with the community
- Explore more advanced MCP features
- Build tools for your specific workflows
Happy coding! 🚀