The bb-mcp-server library provides a built-in documentation endpoint system that allows you to serve documentation files via HTTP. This is particularly useful for:
- Providing documentation to LLMs without filesystem access
- Creating user-friendly documentation portals
- Serving workflow guides and API references
- Supporting both human and machine-readable formats
✨ Content Negotiation: Serve markdown as-is or convert to styled HTML 📦 In-Memory Content: Perfect for compiled applications 🎨 Modern Styling: GitHub-style markdown rendering with syntax highlighting 🔍 Frontmatter Support: Extract metadata for titles and descriptions ⚡ Caching: Optional HTML caching for performance 🔒 Security: Built-in path traversal prevention
Create a module that exports your documentation content:
// src/docs/content.ts
const QUERY_GUIDE = `---
title: Query Workflow Guide
description: Complete guide to using the query workflow
---
# Query Workflow Guide
This guide explains how to use the query workflow...
## Basic Usage
\`\`\`typescript
const result = await workflow.execute({
entity: 'matters',
operation: 'list'
});
\`\`\`
`;
const API_REFERENCE = `---
title: API Reference
description: Complete API documentation
---
# API Reference
## Available Endpoints
### GET /api/v1/workflows
Returns list of available workflows...
`;
const docsMap: Record<string, string> = {
'query-guide': QUERY_GUIDE,
'api-reference': API_REFERENCE,
};
export function get(fileName: string): string | undefined {
return docsMap[fileName];
}
export function list(): string[] {
return Object.keys(docsMap);
}import { AppServer } from '@beyondbetter/bb-mcp-server';
import * as docsContent from './docs/content.ts';
const server = await AppServer.create(async ({ configManager, logger }) => {
return {
docsEndpointConfig: {
enabled: true,
path: '/docs',
contentModule: docsContent,
allowListing: true,
enableCache: true,
},
};
});
await server.start();Browser/HTTP Clients:
http://localhost:3000/docs- List all available docshttp://localhost:3000/docs/query-guide- View as HTMLhttp://localhost:3000/docs/query-guide.md- Download as markdownhttp://localhost:3000/docs/query-guide.html- Explicit HTML request
From LLM/Code:
// Fetch documentation programmatically
const response = await fetch('http://localhost:3000/docs/query-guide.md');
const guide = await response.text();interface DocsEndpointConfig {
/** Enable the docs endpoint */
enabled: boolean;
/** URL path (default: '/docs') */
path: string;
/** Content module (recommended for compiled apps) */
contentModule?: DocsContentModule;
/** Direct content map (alternative to contentModule) */
content?: Map<string, string> | Record<string, string>;
/** Allow directory listing at /docs */
allowListing: boolean;
/** Cache converted HTML in memory */
enableCache: boolean;
}# Enable docs endpoint
DOCS_ENDPOINT_ENABLED=true
# Custom path
DOCS_ENDPOINT_PATH=/documentation
# Allow listing
DOCS_ENDPOINT_ALLOW_LISTING=true
# Enable caching
DOCS_ENDPOINT_ENABLE_CACHE=trueImplement this interface for custom content sources:
interface DocsContentModule {
/**
* Get documentation content by file name (without extension)
*/
get(fileName: string): string | undefined;
/**
* List all available document identifiers
*/
list(): string[];
}Add YAML frontmatter to your markdown for better HTML rendering:
---
title: My Document Title
description: Brief description for meta tags
author: Your Name
date: 2024-10-08
---
# My Document Title
Content goes here...Supported Fields:
title- Used for HTML<title>and headingdescription- Used for HTML<meta name="description">- Any other fields are parsed but not currently used
The endpoint supports three formats:
curl http://localhost:3000/docs/guide.md
# Returns: Raw markdown content
# Content-Type: text/markdowncurl http://localhost:3000/docs/guide.html
curl http://localhost:3000/docs/guide
# Returns: Styled HTML page
# Content-Type: text/htmlcurl http://localhost:3000/docs
# Returns: HTML listing of all documentsFor advanced customization, provide your own handler:
import { DocsEndpointHandler } from '@beyondbetter/bb-mcp-server';
class MyCustomDocsHandler extends DocsEndpointHandler {
// Override methods for custom behavior
async handle(request: Request): Promise<Response> {
// Custom authentication logic
const authHeader = request.headers.get('Authorization');
if (!authHeader) {
return new Response('Unauthorized', { status: 401 });
}
// Call parent handler
return await super.handle(request);
}
}
const server = await AppServer.create(async ({ configManager, logger }) => {
return {
docsEndpointHandler: new MyCustomDocsHandler({
enabled: true,
path: '/docs',
contentModule: docsContent,
allowListing: true,
enableCache: true,
}, logger),
};
});Alternative to content module:
const server = await AppServer.create(async () => {
return {
docsEndpointConfig: {
enabled: true,
path: '/docs',
content: new Map([
['guide', '# Guide\n\nContent here...'],
['api', '# API\n\nAPI docs here...'],
]),
allowListing: true,
enableCache: true,
},
};
});Access cache management methods:
import { getDocsEndpointHandler } from '@beyondbetter/bb-mcp-server';
const handler = getDocsEndpointHandler(configManager, logger, config);
// Get cache statistics
const stats = handler.getCacheStats();
console.log(`Cache size: ${stats.size}`);
console.log(`Cached entries: ${stats.entries.join(', ')}`);
// Clear cache
handler.clearCache();Reference docs in workflow overviews:
class MyWorkflow extends WorkflowBase {
getOverview(): string {
const serverUrl = this.config?.publicUrl || 'http://localhost:3000';
return `
🔍 **My Workflow**
Complete documentation: ${serverUrl}/docs/workflow-guide
## Quick Reference
- Operation guide: ${serverUrl}/docs/operations
- Examples: ${serverUrl}/docs/examples
`.trim();
}
}-
Path Traversal Prevention
- Blocks
..in paths - Rejects absolute paths
- Validates file extensions
- Blocks
-
Content Source Control
- Only serves from configured content
- No filesystem access (when using contentModule)
-
Extension Validation
- Only serves
.mdand.html(converted) - Rejects other file types
- Only serves
// Future feature - design placeholder
interface DocsEndpointConfig {
// ... existing fields ...
// Future: Authentication support
// requireAuth?: boolean;
// allowedRoles?: string[];
// authProvider?: (request: Request) => Promise<boolean>;
}The default HTML rendering includes:
- GitHub-style markdown - Familiar formatting
- Syntax highlighting - Via highlight.js (github-dark theme)
- Responsive design - Works on mobile and desktop
- Dark mode support - Respects
prefers-color-scheme - Clean typography - System fonts for fast loading
For now, the styling is built-in. Future versions will support:
- Custom CSS injection
- Theme selection
- Layout templates
Check configuration:
// Ensure enabled is true
docsEndpointConfig: {
enabled: true, // ← Must be true
// ...
}Check logs:
LOG_LEVEL=debug
# Look for: "DocsEndpointHandler: Initialized"Verify content module:
// Test your content module
import * as docsContent from './docs/content.ts';
console.log(docsContent.list());
console.log(docsContent.get('query-guide'));Check file names:
- Use kebab-case:
query-guidenotqueryGuide - No extensions in module:
'guide'not'guide.md'
Check markdown syntax:
- Ensure valid markdown
- Test with simple content first
Check frontmatter:
---
title: Test
---
# Content- Must have
---delimiters - Must be valid YAML
- Must be at start of file
// main.ts
import { AppServer } from '@beyondbetter/bb-mcp-server';
import * as docsContent from './docs/content.ts';
const server = await AppServer.create(async ({ configManager, logger }) => {
return {
serverConfig: {
name: 'my-mcp-server',
version: '1.0.0',
title: 'My MCP Server',
description: 'Server with documentation',
},
docsEndpointConfig: {
enabled: true,
path: '/docs',
contentModule: docsContent,
allowListing: true,
enableCache: true,
},
};
});
await server.start();
console.log('Server running on http://localhost:3000');
console.log('Documentation at http://localhost:3000/docs');// docs/workflow-docs.ts
export function get(fileName: string): string | undefined {
// Workflow documentation
}
export function list(): string[] { /* ... */ }
// docs/api-docs.ts
export function get(fileName: string): string | undefined {
// API documentation
}
export function list(): string[] { /* ... */ }
// Combine them:
const allDocs: Record<string, string> = {
...Object.fromEntries(workflowDocs.list().map(name => [
`workflow-${name}`,
workflowDocs.get(name)!
])),
...Object.fromEntries(apiDocs.list().map(name => [
`api-${name}`,
apiDocs.get(name)!
])),
};
export function get(fileName: string) {
return allDocs[fileName];
}
export function list() {
return Object.keys(allDocs);
}Planned features for future versions:
// TODO: Future - Generic custom endpoints
interface EndpointHandler {
path: string;
handle(request: Request): Promise<Response>;
}
const server = await AppServer.create(async () => {
return {
customEndpoints: [
new WebhookHandler('/webhook'),
new CustomApiHandler('/custom-api'),
],
};
});// TODO: Future - Load from filesystem directory
docsEndpointConfig: {
enabled: true,
path: '/docs',
directory: './docs', // Load from filesystem
watchForChanges: true, // Hot reload in dev
}// TODO: Future - Built-in auth support
docsEndpointConfig: {
enabled: true,
path: '/docs',
contentModule: docsContent,
requireAuth: true,
allowedRoles: ['admin', 'user'],
}- InstructionsLoader Guide - Similar pattern for MCP instructions
- HTTP Server Guide - HTTP transport configuration
- Plugins (Tools/Workflows) Development Guide - Creating plugins for tools/workflows