Back to Blog

Claude Code Decoded: Documentation Integration

Code shows what. Docs show why. Build an MCP server that automatically surfaces API docs, architecture decisions, and runbooks alongside code for complete context.

Black Dog Labs Team
10/19/2025
15 min read
claude-codeai-developmentmcpdocumentationknowledge-management

Claude Code Decoded: Documentation Integration

You're implementing a new feature in your payment service. The code is straightforward, but there's a comment:

// See payment-processing-guide.md for PCI compliance requirements
async function processPayment(card: CardData): Promise<PaymentResult> {
// ... implementation
}

You ask Claude Code to help add a refund feature. It suggests storing the full card number for easier refunds. That would violate PCI compliance - but Claude has no idea, because it never saw payment-processing-guide.md.

You start pasting the docs into context - the compliance guide, the architecture decisions, the API specs. Then:

claude-code

Session crashed from context overload. You were loading code and documentation, and hit the limit before finishing the implementation. Now you have to decide: do you reload with less code, or less docs?

The gap: Your codebase has two sources of truth:

  1. Code: What the system does
  2. Docs: Why it does it, and how it should be used

Claude only sees one - and loading both efficiently requires strategy.

Why documentation context matters

What lives in docs vs code

Architecture Decision Records (ADRs):

# ADR-015: Use PostgreSQL over MongoDB for user data
## Decision
We chose PostgreSQL for user data storage.
## Rationale
- ACID compliance required for financial transactions
- Complex relational queries on user-order-payment data
- Team expertise in SQL vs NoSQL
## Consequences
- Cannot easily scale horizontally (addressed with read replicas)
- Must carefully design schema migrations
- Better data integrity guarantees
## Alternatives Considered
- MongoDB: Better horizontal scaling, but weak transaction support
- DynamoDB: AWS lock-in, limited query flexibility

API Documentation:

# User Service API
## POST /api/users
**Rate limit:** 100 requests/minute per IP
**Authentication:** Required (JWT)
### Request Body
{
"email": "user@example.com",
"name": "John Doe"
}
### Important Notes
- Email must be unique across the system
- Duplicate email returns 409 Conflict
- Email validation is case-insensitive
- Names support Unicode (full emoji support)
### Error Codes
- 400: Invalid email format
- 409: Email already exists
- 429: Rate limit exceeded

Runbooks and SOPs:

# Incident Response: Database Connection Pool Exhausted
## Symptoms
- 500 errors on API endpoints
- "Connection pool exhausted" in logs
- DB connections stuck in "idle in transaction"
## Immediate Actions
1. Check active connection count: `SELECT count(*) FROM pg_stat_activity`
2. Kill long-running queries: `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle in transaction' AND state_change < NOW() - INTERVAL '5 minutes'`
3. Restart app servers if needed
## Root Causes (Historical)
- Missing transaction commits (3 incidents)
- ORM connection leaks (2 incidents)
- Slow queries holding connections (4 incidents)
## Prevention
- Always use try/finally for connections
- Set connection timeout to 30s max
- Monitor connection pool metrics

Without documentation context, Claude will:

  • Suggest architectures that contradict ADRs
  • Write code that violates API contracts
  • Miss operational considerations from runbooks
  • Ignore compliance requirements from guides

Building the Documentation Integration Server

Architecture

┌─────────────────┐
│ Claude Code │
│ │
│ /docs search │ ← Search docs by topic
│ /docs related │ ← Find docs related to code
│ /docs adr │ ← Load specific ADR
└────────┬────────┘
│ MCP Protocol
┌────────▼────────┐
│ Docs Context │
│ MCP Server │
│ │
│ • Indexing │
│ • Search │
│ • Linking │
└────────┬────────┘
┌────┴────┬────────┬──────────┐
│ │ │ │
┌───▼───┐ ┌──▼──┐ ┌───▼────┐ ┌───▼────┐
│ ADRs │ │ API │ │Runbooks│ │Guides │
│ / │ │docs │ │ / │ │ / │
└───────┘ └─────┘ └────────┘ └────────┘

Implementation

1. Documentation indexer

// src/doc-indexer.ts
import fs from 'fs/promises';
import path from 'path';
import matter from 'gray-matter';
interface DocMetadata {
title: string;
type: 'adr' | 'api' | 'runbook' | 'guide';
tags?: string[];
relatedCode?: string[];
[key: string]: any;
}
interface DocEntry {
path: string;
metadata: DocMetadata;
content: string;
excerpt: string;
}
export class DocIndexer {
private docs: Map<string, DocEntry> = new Map();
private searchIndex: Map<string, Set<string>> = new Map();
async indexDirectory(docsPath: string) {
await this.walkDocs(docsPath);
this.buildSearchIndex();
}
private async walkDocs(dir: string) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
await this.walkDocs(fullPath);
} else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) {
await this.indexDoc(fullPath);
}
}
}
private async indexDoc(filePath: string) {
const content = await fs.readFile(filePath, 'utf-8');
const { data, content: body } = matter(content);
const docEntry: DocEntry = {
path: filePath,
metadata: data as DocMetadata,
content: body,
excerpt: this.generateExcerpt(body),
};
this.docs.set(filePath, docEntry);
}
private generateExcerpt(content: string, length: number = 200): string {
// Remove markdown formatting for cleaner excerpt
const cleaned = content
.replace(/#{1,6}\s/g, '')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/`{1,3}[^`]*`{1,3}/g, '')
.trim();
return cleaned.slice(0, length) + (cleaned.length > length ? '...' : '');
}
private buildSearchIndex() {
for (const [path, doc] of this.docs.entries()) {
// Index by title words
const titleWords = this.tokenize(doc.metadata.title || '');
for (const word of titleWords) {
this.addToIndex(word, path);
}
// Index by tags
if (doc.metadata.tags) {
for (const tag of doc.metadata.tags) {
this.addToIndex(tag.toLowerCase(), path);
}
}
// Index by content (selective - just headers and key terms)
const headers = this.extractHeaders(doc.content);
for (const header of headers) {
const words = this.tokenize(header);
for (const word of words) {
this.addToIndex(word, path);
}
}
}
}
private tokenize(text: string): string[] {
return text
.toLowerCase()
.replace(/[^\w\s]/g, ' ')
.split(/\s+/)
.filter(word => word.length > 2); // Filter short words
}
private extractHeaders(content: string): string[] {
const headerRegex = /^#{1,6}\s+(.+)$/gm;
const headers: string[] = [];
let match;
while ((match = headerRegex.exec(content)) !== null) {
headers.push(match[1]);
}
return headers;
}
private addToIndex(term: string, docPath: string) {
if (!this.searchIndex.has(term)) {
this.searchIndex.set(term, new Set());
}
this.searchIndex.get(term)!.add(docPath);
}
search(query: string, maxResults: number = 10): DocEntry[] {
const queryTerms = this.tokenize(query);
const scores = new Map<string, number>();
// Score documents by term matches
for (const term of queryTerms) {
const matchingDocs = this.searchIndex.get(term);
if (matchingDocs) {
for (const docPath of matchingDocs) {
scores.set(docPath, (scores.get(docPath) || 0) + 1);
}
}
}
// Sort by score and return top results
const sorted = Array.from(scores.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, maxResults)
.map(([path]) => this.docs.get(path)!)
.filter(Boolean);
return sorted;
}
findByType(type: DocMetadata['type']): DocEntry[] {
return Array.from(this.docs.values()).filter(doc => doc.metadata.type === type);
}
findRelatedToCode(codePath: string): DocEntry[] {
return Array.from(this.docs.values()).filter(doc => {
if (!doc.metadata.relatedCode) return false;
return doc.metadata.relatedCode.some(pattern => {
// Support glob-like patterns
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
return regex.test(codePath);
});
});
}
getDoc(docPath: string): DocEntry | null {
return this.docs.get(docPath) || null;
}
}

2. Code-to-docs linker

// src/code-doc-linker.ts
import fs from 'fs/promises';
import { DocIndexer, DocEntry } from './doc-indexer.js';
export class CodeDocLinker {
constructor(private indexer: DocIndexer) {}
async findRelevantDocs(filePath: string): Promise<DocEntry[]> {
const content = await fs.readFile(filePath, 'utf-8');
const relevantDocs: DocEntry[] = [];
// 1. Check for explicit doc references in comments
const docReferences = this.extractDocReferences(content);
for (const ref of docReferences) {
const docs = this.indexer.search(ref);
relevantDocs.push(...docs);
}
// 2. Check for docs with relatedCode patterns
const relatedDocs = this.indexer.findRelatedToCode(filePath);
relevantDocs.push(...relatedDocs);
// 3. Infer from file path and content
const inferredDocs = await this.inferRelevantDocs(filePath, content);
relevantDocs.push(...inferredDocs);
// Deduplicate
const seen = new Set<string>();
return relevantDocs.filter(doc => {
if (seen.has(doc.path)) return false;
seen.add(doc.path);
return true;
});
}
private extractDocReferences(content: string): string[] {
const references: string[] = [];
// Match comments like: // See payment-guide.md
const commentRegex = /\/\/\s*[Ss]ee\s+([a-zA-Z0-9-_]+\.md[x]?)/g;
let match;
while ((match = commentRegex.exec(content)) !== null) {
references.push(match[1]);
}
// Match JSDoc @see tags
const jsdocRegex = /@see\s+([a-zA-Z0-9-_]+\.md[x]?)/g;
while ((match = jsdocRegex.exec(content)) !== null) {
references.push(match[1]);
}
return references;
}
private async inferRelevantDocs(filePath: string, content: string): Promise<DocEntry[]> {
const docs: DocEntry[] = [];
// Infer from file path patterns
if (filePath.includes('/api/') || content.includes('app.post') || content.includes('app.get')) {
docs.push(...this.indexer.findByType('api'));
}
if (filePath.includes('/services/payment') || content.includes('payment')) {
docs.push(...this.indexer.search('payment'));
}
if (filePath.includes('/auth/') || content.includes('authenticate')) {
docs.push(...this.indexer.search('authentication'));
}
// Infer from imports
if (content.includes('from \'@aws-sdk')) {
docs.push(...this.indexer.search('aws infrastructure'));
}
return docs;
}
}

3. MCP Server

// 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';
import { DocIndexer } from './doc-indexer.js';
import { CodeDocLinker } from './code-doc-linker.js';
import path from 'path';
class DocIntegrationMCPServer {
private server: Server;
private indexer: DocIndexer;
private linker: CodeDocLinker;
constructor(private docsPath: string) {
this.server = new Server(
{ name: 'documentation-integration', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
this.indexer = new DocIndexer();
this.linker = new CodeDocLinker(this.indexer);
this.setupHandlers();
}
async initialize() {
console.error('Indexing documentation...');
await this.indexer.indexDirectory(this.docsPath);
console.error('Documentation indexed');
}
private setupHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools:
{
name: 'search_docs',
description: 'Search documentation by keywords or topic',
inputSchema: {
type: 'object',
properties:
{
query: { type: 'string', description: 'Search query' },
max_results: { type: 'number', description: 'Max results', default: 5 },
},
required: ['query'],
},
},
{
name: 'get_doc',
description: 'Get full content of a specific document',
inputSchema: {
type: 'object',
properties: {
doc_path: { type: 'string', description: 'Path to document' },
},
required: ['doc_path'],
},
},
{
name: 'find_related_docs',
description: 'Find documentation related to a code file',
inputSchema: {
type: 'object',
properties: {
code_path: { type: 'string', description: 'Path to code file' },
},
required: ['code_path'],
},
},
{
name: 'list_adrs',
description: 'List all Architecture Decision Records',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'list_runbooks',
description: 'List all runbooks and operational guides',
inputSchema: {
type: 'object',
properties: {},
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === 'search_docs') {
const results = this.indexer.search(
args.query as string,
args.max_results as number | undefined
);
let output = `# Documentation Search: "${args.query}"\n\n`;
output += `Found ${results.length} result(s)\n\n`;
for (const doc of results) {
output += `## ${doc.metadata.title || path.basename(doc.path)}\n`;
output += `**Type:** ${doc.metadata.type}\n`;
output += `**Path:** ${doc.path}\n\n`;
output += `${doc.excerpt}\n\n`;
output += `---\n\n`;
}
return { content: [{ type: 'text', text: output }] };
}
if (name === 'get_doc') {
const doc = this.indexer.getDoc(args.doc_path as string);
if (!doc) {
return {
content: [{ type: 'text', text: `Document not found: ${args.doc_path}` }],
isError: true,
};
}
let output = `# ${doc.metadata.title || path.basename(doc.path)}\n\n`;
if (doc.metadata.tags) {
output += `**Tags:** ${doc.metadata.tags.join(', ')}\n`;
}
output += `**Type:** ${doc.metadata.type}\n\n`;
output += `---\n\n`;
output += doc.content;
return { content: [{ type: 'text', text: output }] };
}
if (name === 'find_related_docs') {
const docs = await this.linker.findRelevantDocs(args.code_path as string);
let output = `# Related Documentation: ${args.code_path}\n\n`;
if (docs.length === 0) {
output += 'No related documentation found.\n';
} else {
for (const doc of docs) {
output += `## ${doc.metadata.title || path.basename(doc.path)}\n`;
output += `**Type:** ${doc.metadata.type}\n`;
output += `**Path:** ${doc.path}\n\n`;
output += `${doc.excerpt}\n\n`;
output += `---\n\n`;
}
}
return { content: [{ type: 'text', text: output }] };
}
if (name === 'list_adrs') {
const adrs = this.indexer.findByType('adr');
let output = `# Architecture Decision Records\n\n`;
output += `Total: ${adrs.length}\n\n`;
for (const adr of adrs) {
output += `- **${adr.metadata.title}** (${path.basename(adr.path)})\n`;
output += ` ${adr.excerpt}\n\n`;
}
return { content: [{ type: 'text', text: output }] };
}
if (name === 'list_runbooks') {
const runbooks = this.indexer.findByType('runbook');
let output = `# Runbooks & Operational Guides\n\n`;
output += `Total: ${runbooks.length}\n\n`;
for (const runbook of runbooks) {
output += `- **${runbook.metadata.title}** (${path.basename(runbook.path)})\n`;
output += ` ${runbook.excerpt}\n\n`;
}
return { content: [{ type: 'text', text: output }] };
}
return {
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
isError: true,
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
isError: true,
};
}
});
}
async run() {
await this.initialize();
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Documentation Integration MCP Server running');
}
}
const docsPath = process.env.DOCS_PATH || path.join(process.cwd(), 'docs');
new DocIntegrationMCPServer(docsPath).run().catch(console.error);

4. Documentation structure

Organize your docs with frontmatter for better indexing:

---
title: "ADR-015: PostgreSQL over MongoDB"
type: adr
tags: [database, architecture, postgresql]
relatedCode:
- "src/db/*"
- "src/models/*"
date: 2024-08-15
status: accepted
---
# ADR-015: Use PostgreSQL over MongoDB for user data
## Decision
We chose PostgreSQL for user data storage...
---
title: "Payment Processing API"
type: api
tags: [payments, api, pci-compliance]
relatedCode:
- "src/services/payment/*"
- "src/api/payment/*"
---
# Payment Processing API
## Security Requirements
All payment processing must follow PCI DSS compliance...
---
title: "Database Connection Pool Exhausted"
type: runbook
tags: [database, incident-response, postgresql]
severity: high
---
# Incident Response: Database Connection Pool Exhausted
## Symptoms
- 500 errors on API endpoints...

Usage examples

Preventing compliance violations

You: Help me add a refund feature to the payment service
# Claude automatically checks related docs
Related Documentation: src/services/payment/process.ts
## Payment Processing Guide
**Type:** guide
**Tags:** payments, pci-compliance
PCI compliance requirements:
- Never store full card numbers
- Use tokenization for refunds
- All payment data must be encrypted...
Claude: "I see this touches payment processing. According to your PCI
compliance guide, we cannot store full card numbers. I'll use
the tokenization service for refund processing..."

Following architecture decisions

You: Should we add a NoSQL database for better scaling?
# search_docs "database choice"
Documentation Search: "database choice"
## ADR-015: PostgreSQL over MongoDB
**Type:** adr
We chose PostgreSQL for user data storage due to ACID compliance
requirements and complex relational queries. Alternative of MongoDB
was rejected due to weak transaction support...
Claude: "According to ADR-015, the team explicitly chose PostgreSQL over
NoSQL for ACID compliance and relational query needs. Before
adding NoSQL, we should understand if those requirements changed."

Operational awareness

You: The app is throwing connection errors. What's happening?
# search_docs "connection pool"
Documentation Search: "connection pool"
## Database Connection Pool Exhausted
**Type:** runbook
Immediate Actions:
1. Check active connections
2. Kill long-running queries
3. Restart if needed
Root causes (historical):
- Missing transaction commits (3 incidents)
- ORM connection leaks (2 incidents)
- Slow queries holding connections (4 incidents)
## Prevention
- Always use try/finally for connections
- Set connection timeout to 30s max
- Monitor connection pool metrics
Claude: "This looks like the connection pool exhaustion incident from
your runbook. Let me check for common causes: missing commits,
connection leaks, or slow queries..."

Best practices

Structuring documentation

1. Use consistent frontmatter

---
title: Clear, descriptive title
type: adr | api | runbook | guide
tags: [relevant, searchable, keywords]
relatedCode: [glob patterns for related files]
---

2. Organize by type

docs/
├── adrs/ # Architecture decisions
├── api/ # API documentation
├── runbooks/ # Operational guides
├── guides/ # Development guides
└── compliance/ # Compliance requirements

3. Link docs to code

// See: docs/guides/payment-processing.md
async function processPayment(data: PaymentData) {
// Implementation follows PCI requirements from guide
}

Maintaining documentation

Keep docs close to code:

# Co-locate docs with services
services/
├── auth-service/
│ ├── src/
│ └── docs/
│ ├── api.md
│ └── architecture.md

Update docs in PRs:

## Pull Request Checklist
- [ ] Code changes implemented
- [ ] Tests added
- [ ] Related docs updated
- [ ] ADR created if architecture changed

Review doc relevance:

# Quarterly: Check for outdated docs
git log --since="6 months ago" --name-only -- docs/
# If files haven't changed but code has, docs may be stale

"Read the docs" is advice we give to junior developers. Then we turn around and give Claude access to code without docs, and wonder why it suggests violating PCI compliance.

Your documentation contains all the context that's too important, too nuanced, or too policy-driven to live in code comments. Architecture decisions. Compliance requirements. API contracts. The runbook for that incident that happens twice a year.

The problem: That documentation lives in Markdown files, Confluence pages, README sections - anywhere except Claude's context window. So you either paste it manually (burning tokens), ignore it (getting bad suggestions), or assume Claude will figure it out (it won't).

Documentation integration closes the loop. Code shows what. Git history shows how. Docs show why, when, and "seriously, don't do this."

The complete context stack: Code + history + docs. Build it once, never explain PCI compliance to an AI again.

Beyond documentation

Documentation integration completes the context picture, giving Claude everything it needs: the code, its evolution, and the human knowledge around it. No more guessing. No more compliance violations. No more "why did you suggest that?"

Just complete context, loaded efficiently, used intelligently.


Series navigation

← Previous: Claude Code Decoded: Git History Context - Using version control history for context

Complete series: