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 requirementsasync 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:
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:
- Code: What the system does
- 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## DecisionWe 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 flexibilityAPI 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 exceededRunbooks 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 Actions1. 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 metricsWithout 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.tsimport 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.tsimport 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.tsimport { 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: adrtags: [database, architecture, postgresql]relatedCode: - "src/db/*" - "src/models/*"date: 2024-08-15status: accepted---# ADR-015: Use PostgreSQL over MongoDB for user data## DecisionWe chose PostgreSQL for user data storage...---title: "Payment Processing API"type: apitags: [payments, api, pci-compliance]relatedCode: - "src/services/payment/*" - "src/api/payment/*"---# Payment Processing API## Security RequirementsAll payment processing must follow PCI DSS compliance...---title: "Database Connection Pool Exhausted"type: runbooktags: [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 docsRelated Documentation: src/services/payment/process.ts## Payment Processing Guide**Type:** guide**Tags:** payments, pci-compliancePCI 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:** adrWe chose PostgreSQL for user data storage due to ACID compliancerequirements and complex relational queries. Alternative of MongoDBwas 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:** runbookImmediate Actions:1. Check active connections2. Kill long-running queries3. Restart if neededRoot 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 metricsClaude: "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 titletype: adr | api | runbook | guidetags: [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 requirements3. Link docs to code
// See: docs/guides/payment-processing.mdasync function processPayment(data: PaymentData) { // Implementation follows PCI requirements from guide}Maintaining documentation
Keep docs close to code:
# Co-locate docs with servicesservices/├── auth-service/│ ├── src/│ └── docs/│ ├── api.md│ └── architecture.mdUpdate docs in PRs:
## Pull Request Checklist- [ ] Code changes implemented- [ ] Tests added- [ ] Related docs updated- [ ] ADR created if architecture changedReview doc relevance:
# Quarterly: Check for outdated docsgit 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:
- Claude Code Decoded: The Token Tax - Understanding token waste patterns
- Claude Code Decoded: The Handoff Protocol - Efficient session handoffs
- Claude Code Decoded: Smart Context Loading - Minimal context loading
- Claude Code Decoded: Multi-Repo Context - Cross-repository context loading
- Claude Code Decoded: Git History Context - Temporal context from version control
- Claude Code Decoded: Documentation Integration - Complete context with docs (this post)
