How to Create Custom MCP Tools
This guide shows you how to develop custom MCP tools for documcp to extend its documentation capabilities.
Prerequisites
- Node.js 20+ and TypeScript knowledge
- Understanding of MCP protocol basics
- documcp development environment set up
- Familiarity with Zod schema validation
MCP Tool Architecture
documcp tools follow the MCP (Model Context Protocol) specification:
interface MCPTool {
name: string;
description: string;
inputSchema: ZodSchema;
handler: (args: any) => Promise<ToolResult>;
}
Step 1: Plan Your Tool
Define Tool Purpose
-
Identify the Documentation Need:
- What documentation task will this tool solve?
- How does it fit into the Diataxis framework?
- What inputs and outputs are required?
-
Create Feature Branch:
git checkout -b feature/tool-your-tool-name
Example Tool Concept
Let's create a validate_markdown_links
tool that checks for broken links in documentation.
Step 2: Create Tool Structure
Tool File Structure
Create your tool in src/tools/
:
touch src/tools/validate-markdown-links.ts
Basic Tool Template
import { z } from 'zod';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
// Input validation schema
const ValidateMarkdownLinksSchema = z.object({
path: z.string().describe('Path to markdown files or directory'),
recursive: z.boolean().optional().default(true).describe('Check subdirectories'),
external: z.boolean().optional().default(false).describe('Validate external URLs'),
timeout: z.number().optional().default(5000).describe('Timeout for external links (ms)')
});
export type ValidateMarkdownLinksArgs = z.infer<typeof ValidateMarkdownLinksSchema>;
export const validateMarkdownLinks: Tool = {
name: 'validate_markdown_links',
description: 'Validate internal and external links in markdown documentation files',
inputSchema: ValidateMarkdownLinksSchema.describe('Validate markdown links tool parameters'),
async handler(args: ValidateMarkdownLinksArgs) {
// Implementation goes here
return {
content: [
{
type: 'text',
text: 'Link validation results...'
}
]
};
}
};
Step 3: Implement Tool Logic
Core Implementation
import fs from 'fs/promises';
import path from 'path';
import { glob } from 'glob';
export const validateMarkdownLinks: Tool = {
name: 'validate_markdown_links',
description: 'Validate internal and external links in markdown documentation files',
inputSchema: ValidateMarkdownLinksSchema,
async handler(args: ValidateMarkdownLinksArgs) {
try {
// Validate input
const validatedArgs = ValidateMarkdownLinksSchema.parse(args);
// Find markdown files
const pattern = path.join(validatedArgs.path, validatedArgs.recursive ? '**/*.md' : '*.md');
const files = await glob(pattern);
const results = {
totalFiles: files.length,
brokenLinks: [] as Array<{
file: string;
link: string;
error: string;
}>,
validLinks: 0
};
// Process each file
for (const file of files) {
const content = await fs.readFile(file, 'utf-8');
const links = extractLinks(content);
for (const link of links) {
try {
await validateLink(link, file, validatedArgs);
results.validLinks++;
} catch (error) {
results.brokenLinks.push({
file: path.relative(process.cwd(), file),
link,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2)
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error validating links: ${error instanceof Error ? error.message : 'Unknown error'}`
}
],
isError: true
};
}
}
};
// Helper functions
function extractLinks(content: string): string[] {
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
const links: string[] = [];
let match;
while ((match = linkRegex.exec(content)) !== null) {
links.push(match[2]);
}
return links;
}
async function validateLink(link: string, sourceFile: string, args: ValidateMarkdownLinksArgs): Promise<void> {
if (link.startsWith('http')) {
if (args.external) {
// Validate external URL
const response = await fetch(link, {
method: 'HEAD',
signal: AbortSignal.timeout(args.timeout || 5000)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
}
} else {
// Validate internal link
const targetPath = path.resolve(path.dirname(sourceFile), link);
try {
await fs.access(targetPath);
} catch {
throw new Error(`File not found: ${link}`);
}
}
}
Step 4: Register Your Tool
Add to Tool Registry
Update src/index.ts
to include your new tool:
import { validateMarkdownLinks } from './tools/validate-markdown-links.js';
// Add to tools array
const tools = [
analyzeRepository,
recommendSSG,
generateConfig,
setupStructure,
populateDiataxisContent,
detectGaps,
validateContent,
validateDiataxisContent,
deployPages,
testLocalDeployment,
verifyDeployment,
validateMarkdownLinks, // Your new tool
];
Step 5: Add Comprehensive Tests
Create Test File
// tests/tools/validate-markdown-links.test.ts
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import { validateMarkdownLinks } from '../../src/tools/validate-markdown-links.js';
import fs from 'fs/promises';
import path from 'path';
describe('validateMarkdownLinks', () => {
const testDir = path.join(__dirname, 'test-markdown');
beforeEach(async () => {
await fs.mkdir(testDir, { recursive: true });
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
});
it('should validate internal links correctly', async () => {
// Create test files
await fs.writeFile(
path.join(testDir, 'test.md'),
'[Link to reference](../reference/api-reference.md)'
);
const result = await validateMarkdownLinks.handler({
path: testDir,
recursive: false,
external: false
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.brokenLinks).toHaveLength(0);
expect(parsed.validLinks).toBe(1);
});
it('should detect broken internal links', async () => {
await fs.writeFile(
path.join(testDir, 'test.md'),
'[Broken link](./nonexistent-file.md)'
);
const result = await validateMarkdownLinks.handler({
path: testDir,
recursive: false,
external: false
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.brokenLinks).toHaveLength(1);
expect(parsed.brokenLinks[0].link).toBe('./nonexistent-file.md');
});
});
Run Tests
npm test -- validate-markdown-links
Step 6: Add Documentation
Tool Documentation
Create documentation in docs/reference/tools/
:
# validate_markdown_links
Validates internal and external links in markdown documentation files.
## Parameters
- `path` (string): Path to markdown files or directory
- `recursive` (boolean, optional): Check subdirectories (default: true)
- `external` (boolean, optional): Validate external URLs (default: false)
- `timeout` (number, optional): Timeout for external links in ms (default: 5000)
## Example Usage
```json
{
"name": "validate_markdown_links",
"arguments": {
"path": "./docs",
"recursive": true,
"external": true,
"timeout": 10000
}
}
Return Format
Returns JSON with validation results:
totalFiles
: Number of files processedbrokenLinks
: Array of broken link detailsvalidLinks
: Count of valid links found
### Update API Reference
Add your tool to `docs/reference/api-reference.md`:
```markdown
### validate_markdown_links
**Purpose**: Validate internal and external links in markdown files
**Use Cases**:
- Documentation quality assurance
- Pre-deployment link validation
- Maintenance of large documentation sites
**Integration**: Works with all SSGs and documentation frameworks
Step 7: Integration Testing
Test with AI Client
Test your tool with Claude Desktop or other MCP clients:
Can you validate all the links in my documentation using the validate_markdown_links tool?
Performance Testing
Test with large documentation sets:
# Create performance test
DEBUG=documcp:performance npm run dev
Best Practices
Code Quality
- Input Validation: Always use Zod schemas for type safety
- Error Handling: Provide clear, actionable error messages
- Performance: Consider memory usage for large repositories
- Security: Validate file paths and prevent directory traversal
Tool Design
- Single Responsibility: Each tool should have one clear purpose
- Composability: Tools should work well together
- Consistency: Follow existing tool patterns and naming
- Documentation: Provide comprehensive usage examples
Testing Strategy
- Unit Tests: Test core logic in isolation
- Integration Tests: Test with real file systems
- Error Cases: Test all failure scenarios
- Performance Tests: Validate with large datasets
Advanced Features
Tool Chaining
Design tools to work together:
// Tool that uses results from other tools
export const generateQualityReport: Tool = {
name: 'generate_quality_report',
description: 'Generate comprehensive documentation quality report',
async handler(args) {
// Use multiple tools together
const gapAnalysis = await detectGaps.handler(args);
const linkValidation = await validateMarkdownLinks.handler(args);
const contentValidation = await validateContent.handler(args);
// Combine results
return combineReports(gapAnalysis, linkValidation, contentValidation);
}
};
Configuration Support
Add tool-specific configuration:
const config = {
linkValidation: {
timeout: process.env.LINK_TIMEOUT || 5000,
retries: process.env.LINK_RETRIES || 3,
userAgent: process.env.USER_AGENT || 'documcp-validator'
}
};
Deployment and Distribution
Build and Test
npm run build
npm test
npm run lint
Create Pull Request
git add .
git commit -m "feat: add validate_markdown_links tool
- Validates internal and external markdown links
- Supports recursive directory scanning
- Configurable timeout for external URLs
- Comprehensive test coverage"
git push origin feature/tool-validate-markdown-links