MCP Framework
Docs Package

Custom Source Adapters

Building custom source adapters for non-standard documentation backends

Custom Source Adapters

If your documentation doesn't use Fumadocs or llms.txt, you can build a custom source adapter by implementing the DocSource interface.

Implementing DocSource

import {
  DocSource,
  DocPage,
  DocSearchResult,
  DocSection,
  DocSearchOptions,
} from "@mcpframework/docs";

class MyCustomSource implements DocSource {
  name = "my-custom-docs";

  async search(query: string, options?: DocSearchOptions): Promise<DocSearchResult[]> {
    const limit = options?.limit ?? 10;
    const section = options?.section;

    // Call your search backend
    const response = await fetch(`https://api.example.com/search?q=${query}`);
    const data = await response.json();

    return data.results.slice(0, limit).map((item: any) => ({
      slug: item.path,
      url: `https://docs.example.com/${item.path}`,
      title: item.title,
      snippet: item.excerpt,
      section: item.category,
      score: item.relevance,
    }));
  }

  async getPage(slug: string): Promise<DocPage | null> {
    try {
      const response = await fetch(`https://api.example.com/pages/${slug}`);
      if (!response.ok) return null;

      const data = await response.json();
      return {
        slug,
        url: `https://docs.example.com/${slug}`,
        title: data.title,
        content: data.markdown,
        section: data.category,
      };
    } catch {
      return null;
    }
  }

  async listSections(): Promise<DocSection[]> {
    const response = await fetch("https://api.example.com/sections");
    const data = await response.json();

    return data.map((s: any) => ({
      name: s.title,
      slug: s.id,
      url: `https://docs.example.com/${s.id}`,
      children: (s.subsections || []).map((sub: any) => ({
        name: sub.title,
        slug: sub.id,
        url: `https://docs.example.com/${s.id}/${sub.id}`,
        children: [],
        pageCount: sub.page_count,
      })),
      pageCount: s.page_count,
    }));
  }

  async getIndex(): Promise<string> {
    // Return llms.txt-formatted content, or empty string
    return "";
  }

  async getFullContent(): Promise<string> {
    // Return all docs concatenated, or empty string
    return "";
  }

  async healthCheck(): Promise<{ ok: boolean; message?: string }> {
    try {
      const response = await fetch("https://api.example.com/health");
      return { ok: response.ok };
    } catch (error) {
      return { ok: false, message: (error as Error).message };
    }
  }
}

Using Your Custom Source

import { DocsServer } from "@mcpframework/docs";

const source = new MyCustomSource();

const server = new DocsServer({
  source,
  name: "my-custom-docs",
  version: "1.0.0",
});

server.start();

Extending LlmsTxtSource

If your site publishes llms.txt but also has a custom search API, extend LlmsTxtSource instead of implementing from scratch:

import { LlmsTxtSource, DocSearchResult, DocSearchOptions } from "@mcpframework/docs";

class MyEnhancedSource extends LlmsTxtSource {
  override get name(): string {
    return `enhanced:${this.baseUrl}`;
  }

  override async search(
    query: string,
    options?: DocSearchOptions
  ): Promise<DocSearchResult[]> {
    try {
      // Try your custom search API first
      const response = await fetch(`${this.baseUrl}/api/my-search?q=${query}`);
      if (response.ok) {
        const data = await response.json();
        return this.mapResults(data);
      }
    } catch {
      // Fall back to local text search
    }

    return super.search(query, options);
  }

  private mapResults(data: any[]): DocSearchResult[] {
    return data.map((item, i) => ({
      slug: item.slug,
      url: item.url,
      title: item.title,
      snippet: item.excerpt || "",
      score: 1 - i / data.length,
    }));
  }
}

Error Handling

Use the built-in error classes for consistency:

import {
  DocSourceError,
  DocFetchError,
  DocParseError,
  DocNotFoundError,
} from "@mcpframework/docs";

class MySource implements DocSource {
  async getPage(slug: string): Promise<DocPage | null> {
    const response = await fetch(`https://api.example.com/pages/${slug}`);

    if (response.status === 404) {
      return null; // Not found -- return null, don't throw
    }

    if (!response.ok) {
      throw new DocFetchError(
        `https://api.example.com/pages/${slug}`,
        response.status,
        response.statusText
      );
    }

    const data = await response.json();
    if (!data.markdown) {
      throw new DocParseError(`Page ${slug} has no markdown content`);
    }

    return { slug, url: data.url, title: data.title, content: data.markdown };
  }
  // ... other methods
}

The tools catch DocSourceError subclasses and return user-friendly messages instead of exposing stack traces to the MCP client.