Skip to main content
doc0blade47/mem0documentation home
Sign in

Documentation Agent Skills

Last updated Oct 11, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Documentation Agent Skills provide specialized capabilities and tooling that empower coding assistants to retrieve, verify, and maintain technical documentation and contextual knowledge. By establishing lightweight, on-demand search engines and verification scripts, the architecture prevents context bloat in developer environments while keeping reference materials accessible. Sources: skills/mem0/scripts/mem0_doc_search.py:3-21, scripts/check-llms-txt-coverage.py:2-22

These skills bridge documentation distribution formats—such as llms.txt indices and Markdown source trees—with interactive agent runtimes and local Model Context Protocol servers. Through dynamic skill resolution, automated coverage audits, and structured prompt rendering, agents can query specific reference pages, audit missing documentation links, and expose semantic memory search tools uniformly across various IDE plugins. Sources: skills/mem0/scripts/mem0_doc_search.py:6-7, integrations/openclaw/skill-loader.ts:323-327, integrations/agent-plugin-core/python/mcp_server.py:2-3, scripts/check-llms-txt-coverage.py:2-7

Documentation search tool architecture

Engine configuration and security boundaries

The documentation search utility defined in mem0_doc_search.py operates as an on-demand retrieval engine that queries https://docs.mem0.ai/llms.txt and fetches remote Markdown documentation pages. To prevent unauthorized redirection or arbitrary payload ingestion, hardcoded security constants govern all outgoing HTTP connections. The host domain docs.mem0.ai is enforced through URL splitting checks, and custom HTTP redirect handlers intercept intermediate location headers to verify that subsequent requests remain bound to the trusted host.

DOCS_BASE = "https://docs.mem0.ai"
DOCS_HOST = urllib.parse.urlsplit(DOCS_BASE).netloc
LLMS_INDEX = f"{DOCS_BASE}/llms.txt"
ENTRY_URL = re.compile(r"\((https?://[^)\s]+)\)")
MAX_RESULTS = 20
MAX_PAGE_CHARS = 10000
MAX_INDEX_BYTES = 2_000_000
 
SECTION_PREFIXES = {
    "platform": ("/platform/",),
    "api": ("/api-reference/",),
    "open-source": ("/open-source/", "/components/"),
    "integrations": ("/integrations",),
}
 
 
def require_docs_url(url: str) -> None:
    """Exit unless the URL is an https URL on the Mem0 docs host."""
    parts = urllib.parse.urlsplit(url)
    if parts.scheme != "https" or parts.netloc != DOCS_HOST:
        sys.exit(f"Refusing to fetch {url}: only {DOCS_BASE} is allowed")
 
 
class DocsRedirectHandler(urllib.request.HTTPRedirectHandler):
    """Follow redirects only when they stay on the Mem0 docs host."""
 
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        require_docs_url(newurl)
        return super().redirect_request(req, fp, code, msg, headers, newurl)
 

Sources: skills/mem0/scripts/mem0_doc_search.py:33-62

The engine enforces strict resource boundaries during HTTP retrieval operations. Byte-size caps and truncation limits prevent denial-of-service conditions or excessive memory consumption when reading remote index files or documentation assets over the network.

Constant NameValuePurpose
DOCS_BASEhttps://docs.mem0.aiCanonical documentation root URL
MAX_RESULTS20Maximum matching search URLs returned in query payloads
MAX_PAGE_CHARS10000Character truncation threshold for individual documentation pages
MAX_INDEX_BYTES2_000_000Maximum byte size allowed for the llms.txt index file
DOCS_BASE = "https://docs.mem0.ai"
DOCS_HOST = urllib.parse.urlsplit(DOCS_BASE).netloc
LLMS_INDEX = f"{DOCS_BASE}/llms.txt"
ENTRY_URL = re.compile(r"\((https?://[^)\s]+)\)")
MAX_RESULTS = 20
MAX_PAGE_CHARS = 10000
MAX_INDEX_BYTES = 2_000_000

Sources: skills/mem0/scripts/mem0_doc_search.py:33-39

Caution

Bypassing require_docs_url or removing the DocsRedirectHandler check allows open redirect vectors to fetch arbitrary external content into the agent context stream. Every redirect is validated against DOCS_HOST before the request is dispatched.

def require_docs_url(url: str) -> None:
    """Exit unless the URL is an https URL on the Mem0 docs host."""
    parts = urllib.parse.urlsplit(url)
    if parts.scheme != "https" or parts.netloc != DOCS_HOST:
        sys.exit(f"Refusing to fetch {url}: only {DOCS_BASE} is allowed")
 
 
class DocsRedirectHandler(urllib.request.HTTPRedirectHandler):
    """Follow redirects only when they stay on the Mem0 docs host."""
 
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        require_docs_url(newurl)
        return super().redirect_request(req, fp, code, msg, headers, newurl)

Sources: skills/mem0/scripts/mem0_doc_search.py:49-61

Section filtering and token scoring mechanics

Documentation search queries are processed by tokenizing the input string using regular expression matching and scoring index entries based on frequency and title appearance. When a section filter is supplied, index entries are pre-filtered against predefined URL prefix mappings before score calculation occurs.

SECTION_PREFIXES = {
    "platform": ("/platform/",),
    "api": ("/api-reference/",),
    "open-source": ("/open-source/", "/components/"),
    "integrations": ("/integrations",),
}

Sources: skills/mem0/scripts/mem0_doc_search.py:41-46

The engine maps user-facing section keys to their corresponding documentation route paths. The supported section mappings and URL prefixes are structured as follows:

Section KeyURL PrefixesTarget Documentation Area
platform("/platform/",)Platform features and managed services
api("/api-reference/",)REST API specifications and endpoints
open-source("/open-source/", "/components/")Open-source core libraries and modular components
integrations("/integrations",)Third-party integrations and agent frameworks
SECTION_PREFIXES = {
    "platform": ("/platform/",),
    "api": ("/api-reference/",),
    "open-source": ("/open-source/", "/components/"),
    "integrations": ("/integrations",),
}

Sources: skills/mem0/scripts/mem0_doc_search.py:41-46

Note

The search scoring algorithm evaluates both general occurrence in the entry string and specific appearance in the page title segment, doubling points for matches occurring in the title substring before sorting results in descending order.

def search_docs(query: str, section: str | None = None) -> dict:
    """Search the llms.txt index for entries matching the query terms, best matches first."""
    terms = re.findall(r"[a-z0-9_]+", query.lower())
    entries = index_entries()
 
    if section in SECTION_PREFIXES:
        entries = [e for e in entries if entry_path(e).startswith(SECTION_PREFIXES[section])]
 
    scored = []
    for entry in entries:
        haystack = entry.lower()
        title = haystack.split("]")[0]
        score = sum((term in haystack) + (term in title) for term in terms)
        if score:
            scored.append((score, entry))
    scored.sort(key=lambda item: -item[0])
 
    return {
        "source": "llms_txt_index",
        "query": query,
        "matching_urls": [entry for _, entry in scored[:MAX_RESULTS]],
        "suggestion": "Fetch specific pages with --page <path> for detailed content",
    }

Sources: skills/mem0/scripts/mem0_doc_search.py:93-115

Documentation index parsing and retrieval

Index extraction and entry parsing

The retrieval engine processes the remote llms.txt document index by fetching the raw bytes and parsing line-item entries that conform to Markdown link structures. The index_entries function verifies that the fetched payload does not exceed MAX_INDEX_BYTES before splitting lines and extracting targets beginning with - [.

def index_entries() -> list:
    """Return the page entries listed in the llms.txt index."""
    content, truncated = fetch_url(LLMS_INDEX, MAX_INDEX_BYTES)
    if truncated:
        sys.exit(f"Error: {LLMS_INDEX} exceeds {MAX_INDEX_BYTES} bytes")
    return [line.strip()[2:] for line in content.splitlines() if line.strip().startswith("- [")]
 

Sources: skills/mem0/scripts/mem0_doc_search.py:79-85

Individual index lines are evaluated using a compiled regular expression pattern ENTRY_URL that isolates target URLs enclosed in parentheses. The helper function entry_path parses the matched URI to extract its relative URL path for downstream prefix filtering.

skills/mem0/scripts/mem0_doc_search.py:36| ENTRY_URL = re.compile(r"\((https?://[^)\s]+)\)")
skills/mem0/scripts/mem0_doc_search.py:87-91

Sources: skills/mem0/scripts/mem0_doc_search.py:36, skills/mem0/scripts/mem0_doc_search.py:87-91

Warning

If the llms.txt payload exceeds 2 megabytes, index_entries immediately terminates execution via sys.exit to prevent excessive heap allocation during remote agent execution.

content, truncated = fetch_url(LLMS_INDEX, MAX_INDEX_BYTES)
if truncated:
    sys.exit(f"Error: {LLMS_INDEX} exceeds {MAX_INDEX_BYTES} bytes")

Sources: skills/mem0/scripts/mem0_doc_search.py:81-83

Page fetching and markdown resolution

When an agent requests a specific documentation page via path parameter, fetch_page constructs the target URL by joining DOCS_BASE with the requested path, ensuring that missing .md file extensions are automatically appended.

def fetch_page(page_path: str) -> dict:
    """Fetch a specific documentation page as markdown."""
    parts = urllib.parse.urlsplit(urllib.parse.urljoin(DOCS_BASE, page_path))
    path = parts.path.rstrip("/")
    if not path.endswith(".md"):
        path = f"{path}.md"
    url = urllib.parse.urlunsplit((parts.scheme, parts.netloc, path, "", ""))

Sources: skills/mem0/scripts/mem0_doc_search.py:118-124

Fetched content is bounded by MAX_PAGE_CHARS multiplied by four during retrieval to detect downstream truncation, returning a dictionary containing the resolved URL, truncated content string, and a boolean flag indicating whether size limits were exceeded.

skills/mem0/scripts/mem0_doc_search.py:38| MAX_PAGE_CHARS = 10000
skills/mem0/scripts/mem0_doc_search.py:125-126

Sources: skills/mem0/scripts/mem0_doc_search.py:38, skills/mem0/scripts/mem0_doc_search.py:125-126

Function NameReturn TypePurpose
get_index()dictReturns total page count, full URL list, and available section keys
list_section(section)dictReturns matching index entries for a validated section or error payload
fetch_page(page_path)dictResolves .md routes and retrieves markdown content with truncation status
def get_index() -> dict:
    """Fetch the full documentation index from llms.txt."""
    urls = index_entries()
    return {"total_pages": len(urls), "urls": urls, "sections": list(SECTION_PREFIXES)}
 
 
def list_section(section: str) -> dict:
    """List the llms.txt index entries in a documentation section."""
    if section not in SECTION_PREFIXES:
        return {"error": f"Unknown section: {section}", "available": list(SECTION_PREFIXES)}
    pages = [e for e in index_entries() if entry_path(e).startswith(SECTION_PREFIXES[section])]
    return {"section": section, "pages": pages}
 

Sources: skills/mem0/scripts/mem0_doc_search.py:129-141

Agent skill definition and loading

Skill resolution and triage prompt loading

The OpenClaw skill loading mechanism dynamically resolves skill files such as memory-triage, merges domain-specific overlays, and handles user configuration knobs to construct injectable system prompts. The function loadSkill reads the skill file, parses its frontmatter and body, and incorporates configuration parameters like categories, importance thresholds, credential scanning patterns, and custom inclusion or exclusion rules.

export function loadSkill(
  skillName: string,
  config: SkillsConfig = {},
): LoadedSkill | null {
  const raw = readSkillFile(skillName);
  if (!raw) return null;
 
  const parsed = parseSkillFile(raw);
  const parts: string[] = [parsed.body];
 
  // Domain overlays only apply to the skill they target (checked via applies_to)
  if (config.domain) {
    const overlay = readDomainOverlay(config.domain, skillName);
    if (overlay) {
      parts.push("\n" + overlay);
    }
  }
 
  // Inject user-configured categories into triage skill prompt
  if (skillName === "memory-triage" && config.categories) {
    const mergedCats = resolveCategories(config);
    parts.push(renderCategoriesBlock(mergedCats));
  }
 
  // Inject triage knobs (importanceThreshold, credentialPatterns)
  if (skillName === "memory-triage") {
    const knobs = renderTriageKnobs(config);
    if (knobs) parts.push(knobs);
  }
 
  // Append user custom rules to the triage prompt.
  if (skillName === "memory-triage" && config.customRules) {
    const rulesBlock: string[] = ["\n## User Custom Rules\n"];
    if (config.customRules.include?.length) {
      rulesBlock.push("Additionally extract:");
      for (const rule of config.customRules.include) {
        rulesBlock.push(`- ${rule}`);
      }
    }
    if (config.customRules.exclude?.length) {
      rulesBlock.push("\nAdditionally skip:");
      for (const rule of config.customRules.exclude) {
        rulesBlock.push(`- ${rule}`);
      }
    }
    parts.push(rulesBlock.join("\n"));
  }
 
  return {
    name: skillName,
    prompt: parts.join("\n"),
    frontmatter: parsed.frontmatter,
  };
}

Sources: integrations/openclaw/skill-loader.ts:324-377

Note

Domain overlays apply exclusively to the specific skill they target by evaluating the applies_to attribute during file parsing, ensuring that domain-specific guidelines do not bleed into unrelated agent skills.

if (config.domain) {
  const overlay = readDomainOverlay(config.domain, skillName);
  if (overlay) {
    parts.push("\n" + overlay);
  }
}

Sources: integrations/openclaw/skill-loader.ts:335-340

Config injection and rule rendering

User configurations such as active categories and custom extraction rules are formatted into markdown blocks and appended directly to the memory-triage skill prompt. When custom rules exceed the character budget defined by COMPACT_CUSTOM_RULE_CHAR_BUDGET, the loader condenses the rule text into a compact summary view containing rule counts and truncated previews.

function renderCompactCustomRules(config: SkillsConfig): string[] {
  const includeRules = config.customRules?.include ?? [];
  const excludeRules = config.customRules?.exclude ?? [];
  if (includeRules.length === 0 && excludeRules.length === 0) {
    return [];
  }
 
  const formatRuleList = (label: string, rules: string[]) =>
    `${label}: ${rules.map((rule) => `"${rule}"`).join("; ")}`;
 
  const lines: string[] = [];
  if (includeRules.length > 0) {
    lines.push(formatRuleList("Include", includeRules));
  }
  if (excludeRules.length > 0) {
    lines.push(formatRuleList("Exclude", excludeRules));
  }
 
  const combined = lines.join(" ");
  if (combined.length <= COMPACT_CUSTOM_RULE_CHAR_BUDGET) {
    return ["## Active Custom Rules", "", ...lines.map((line) => `- ${line}`)];
  }
 
  const preview = [
    ...includeRules.slice(0, COMPACT_CUSTOM_RULE_PREVIEW_LIMIT).map((rule) => `include "${rule}"`),
    ...excludeRules.slice(0, COMPACT_CUSTOM_RULE_PREVIEW_LIMIT).map((rule) => `exclude "${rule}"`),
  ];
 
  return [
    "## Active Custom Rules",
    "",
    `- ${includeRules.length} include rule(s) and ${excludeRules.length} exclude rule(s) are configured.`,
    `- Prompt kept compact, full rule text omitted because it exceeded ${COMPACT_CUSTOM_RULE_CHAR_BUDGET} characters.`,
    ...(preview.length > 0
      ? [`- Preview: ${preview.join("; ")}`]
      : []),
  ];
}

Sources: integrations/openclaw/skill-loader.ts:256-293

Warning

If custom rule descriptions exceed 280 characters, full rule text is omitted from the prompt injection, replaced by a count summary and a preview limited to two items per rule category to prevent prompt bloat.

const COMPACT_CUSTOM_RULE_CHAR_BUDGET = 280;
const COMPACT_CUSTOM_RULE_PREVIEW_LIMIT = 2;
…
if (combined.length <= COMPACT_CUSTOM_RULE_CHAR_BUDGET) {
  return ["## Active Custom Rules", "", ...lines.map((line) => `- ${line}`)];
}
 
const preview = [
  ...includeRules.slice(0, COMPACT_CUSTOM_RULE_PREVIEW_LIMIT).map((rule) => `include "${rule}"`),
  ...excludeRules.slice(0, COMPACT_CUSTOM_RULE_PREVIEW_LIMIT).map((rule) => `exclude "${rule}"`),
];
 
return [
  "## Active Custom Rules",
  "",
  `- ${includeRules.length} include rule(s) and ${excludeRules.length} exclude rule(s) are configured.`,
  `- Prompt kept compact, full rule text omitted because it exceeded ${COMPACT_CUSTOM_RULE_CHAR_BUDGET} characters.`,
  ...(preview.length > 0
    ? [`- Preview: ${preview.join("; ")}`]
    : []),
];

Sources: integrations/openclaw/skill-loader.ts:229-230, integrations/openclaw/skill-loader.ts:275-292

Lifecycle integration and recall strategy

During the before_prompt_build lifecycle hook in OpenClaw, the plugin evaluates session triggers and injects the loaded triage protocol into prependSystemContext for provider-level caching. Depending on the configured recall strategy, the agent executes semantic searches against long-term or session-scoped memory stores and injects retrieved context into prependContext on every turn.

export function loadTriagePrompt(config: SkillsConfig = {}): string {
  // Try to load the full skill with all config-driven overlays
…
let systemContext = loadCompactTriagePrompt(cfg.skills ?? {});
if (isSubagent) {
  systemContext =
    "You are a subagent — use these memories for context but do not assume you are this user. Do NOT store new memories.\n\n" +
    systemContext;
}
 
// Dynamic recall goes in prependContext (changes every turn).
// Strategy controls how much the plugin searches automatically:
//   "always" — long-term + session search every turn (2 searches)
//   "smart"  — long-term search only, no session search (1 search) [default]
//   "manual" — no auto-recall; agent controls all search via memory_search (0 searches)
let recallContext = "";
const recallEnabled = cfg.skills?.recall?.enabled !== false;
const recallStrategy = cfg.skills?.recall?.strategy ?? "smart";
 
if (recallEnabled && recallStrategy !== "manual") {
  const recallStart = Date.now();
  try {
    const query = lifecycle.prepareUserText(sanitizeQuery(event.prompt));
 
    // Smart mode: skip session search (saves 1 API call per turn)
    const sessionIdForRecall =
      recallStrategy === "always"
        ? isSubagent
          ? undefined
          : sessionId
        : undefined; // smart: long-term only
 
    const recallResult = await skillRecall(
      provider,
      query,
      userId,
      cfg.skills ?? {},
      sessionIdForRecall,
    );
 
    api.logger.info(
      `openclaw-mem0: skills-mode recall (strategy=${recallStrategy}) injecting ${recallResult.memories.length} memories (~${recallResult.tokenEstimate} tokens)`,
    );
 
    _captureEvent("openclaw.hook.recall", {
      strategy: recallStrategy,
      memory_count: recallResult.memories.length,
      latency_ms: Date.now() - recallStart,
    });
 
    recallContext = recallResult.context;
  } catch (err) {
    api.logger.warn(
      `openclaw-mem0: skills-mode recall failed: ${String(err)}`,
    );
  }
} else if (recallEnabled && recallStrategy === "manual") {
  api.logger.info(
    "openclaw-mem0: skills-mode recall strategy=manual, agent controls search",
  );
}
 
return {
  prependSystemContext: systemContext, // cached by provider
  prependContext: recallContext, // per-turn dynamic
};

Sources: integrations/openclaw/skill-loader.ts:390-391, integrations/openclaw/index.ts:467-529

Recall StrategySession Search BehaviorTarget Context PropertySearch Cost per Turn
"always"Searches both long-term and session storesprependContext2 search calls
"smart"Searches long-term memory store onlyprependContext1 search call
"manual"Disables automatic recall completelyNone0 search calls
//   "always" — long-term + session search every turn (2 searches)
//   "smart"  — long-term search only, no session search (1 search) [default]
//   "manual" — no auto-recall; agent controls all search via memory_search (0 searches)
let recallContext = "";
…
const sessionIdForRecall =
  recallStrategy === "always"
    ? isSubagent
      ? undefined
      : sessionId
    : undefined; // smart: long-term only

Sources: integrations/openclaw/index.ts:476-479, integrations/openclaw/index.ts:489-494

Documentation coverage verification workflows

Two-way diff audit mechanisms

The coverage verification script scripts/check-llms-txt-coverage.py executes a two-way diff between documentation source files on disk and indexed URLs in docs/llms.txt. It scans docs/**/*.mdx to construct a canonical set of repository pages, filters out paths matching ignore prefixes loaded from scripts/llms-txt-ignore.txt, and parses indexed URLs matching https://docs.mem0.ai/ to identify missing entries and stale links.

def load_ignore_prefixes() -> list[str]:
    if not IGNORE_FILE.exists():
        return []
    prefixes: list[str] = []
    for raw in IGNORE_FILE.read_text().splitlines():
        line = raw.strip()
        if line and not line.startswith("#"):
            prefixes.append(line)
    return prefixes
 
 
def canonical_repo_pages(ignore_prefixes: list[str]) -> tuple[set[str], set[str]]:
    all_pages: set[str] = set()
    included_pages: set[str] = set()
    for path in DOCS_DIR.rglob("*.mdx"):
        rel = path.relative_to(DOCS_DIR).with_suffix("").as_posix()
        all_pages.add(rel)
        if not any(rel.startswith(p) for p in ignore_prefixes):
            included_pages.add(rel)
    return all_pages, included_pages
 
 
def indexed_urls(text: str) -> set[str]:
    urls: set[str] = set()
    for match in URL_RE.finditer(text):
        path = match.group(1).rstrip("/")
        if path:
            urls.add(path)
    return urls

Sources: scripts/check-llms-txt-coverage.py:41-69

Drift TypeDetection LogicDefault Exit Code--write Mode Behavior
missingincluded_pages - linked1Appends placeholders under ## Unclassified - needs triage
stalelinked - all_pages1Reported in output, but never auto-removed
default   read-only; exits 1 if any drift is found.
--write   appends placeholder entries for missing pages under a
          "## Unclassified - needs triage" H2 at the end of
          docs/llms.txt. Stale URLs are reported but never removed
          automatically (human decides whether a page was renamed
          or genuinely deleted). Exits 1 if anything was written,
          0 if the file was already in sync.
…
missing = sorted(included_pages - linked)
stale = sorted(linked - all_pages)

Sources: scripts/check-llms-txt-coverage.py:9-15, scripts/check-llms-txt-coverage.py:111-112

Caution

Stale URLs in docs/llms.txt are never automatically removed when running with --write because human intervention is required to determine whether a page was renamed or genuinely deleted.

docs/llms.txt. Stale URLs are reported but never removed
automatically (human decides whether a page was renamed
or genuinely deleted). Exits 1 if anything was written,
…
print(
    "\n(Stale URLs are never auto-removed: check whether the page was "
    "renamed, and update the link by hand.)"
)

Sources: scripts/check-llms-txt-coverage.py:12-14, scripts/check-llms-txt-coverage.py:122-125

Scaffolding and triage block generation

When invoked with --write, the script formats placeholder entries for every missing documentation page and appends them to docs/llms.txt. If the triage header ## Unclassified - needs triage is already present, new entries are appended directly beneath it; otherwise, a complete preamble instructing maintainers to classify scopes and rewrite descriptions is inserted before the placeholder list.

def format_placeholder(page: str) -> str:
    title = page.rsplit("/", 1)[-1].replace("-", " ").replace("_", " ").title()
    return (
        f"- [{title}]({BASE_URL}{page}) [TODO: Platform|OSS|Both]: "
        "TODO - rewrite as 'Use when ...' and move into the correct section."
    )
 
 
def append_triage_block(text: str, missing_pages: list[str]) -> str:
    entries = "\n".join(format_placeholder(p) for p in missing_pages)
    if TRIAGE_HEADER in text:
        return text.rstrip() + "\n" + entries + "\n"
    preamble = (
        "> New docs pages were detected by CI. For each entry below: replace "
        "the scope tag with `[Platform]`, `[OSS]`, or `[Both]`; rewrite the "
        "description as `Use when ...`; move it into the correct section; "
        "then delete this H2 once empty.\n"
    )
    return text.rstrip() + "\n\n" + TRIAGE_HEADER + "\n\n" + preamble + "\n" + entries + "\n"
 
…
if args.write:
    if missing:
        new_text = append_triage_block(text, missing)
        LLMS_TXT.write_text(new_text)
        print(
            f"\nAppended {len(missing)} placeholder entries under "
            f"'{TRIAGE_HEADER}' in docs/llms.txt."
        )

Sources: scripts/check-llms-txt-coverage.py:72-91, scripts/check-llms-txt-coverage.py:131-138

def format_placeholder(page: str) -> str:
    title = page.rsplit("/", 1)[-1].replace("-", " ").replace("_", " ").title()
    return (
        f"- [{title}]({BASE_URL}{page}) [TODO: Platform|OSS|Both]: "
        "TODO - rewrite as 'Use when ...' and move into the correct section."
    )

Sources: scripts/check-llms-txt-coverage.py:72-77

Coding assistant MCP server interfaces

JSON-RPC protocol handling and tool registration

Plugin harnesses such as Claude Code Plugin implement a local Model Context Protocol (MCP) server over standard input and output streams using Python core scripts like integrations/claude-code-plugin/core/mcp_server.py. The server runs on protocol version 2024-11-05, declaring the search_memories tool with read-only, idempotent, and open-world hints. Incoming JSON-RPC requests are parsed line by line from sys.stdin, routing methods such as initialize, ping, tools/list, and tools/call to appropriate handlers while catching deserialization and internal errors.

PROTOCOL_VERSION = "2024-11-05"
TOOL_NAME = "search_memories"
TOOL_DESCRIPTION = (
    "Search memories from earlier work in this repository. Use it before "
    "repeating investigation or when earlier decisions, fixes, commands, or "
    "results may help."
)
…
def handle_request(message: Any) -> dict[str, Any] | None:
    if not isinstance(message, dict):
        return None
    request_id = message.get("id")
    method = message.get("method")
 
    if method == "notifications/initialized":
        return None
    if method == "initialize":
        requested = (message.get("params") or {}).get("protocolVersion")
        return {
            "jsonrpc": "2.0",
            "id": request_id,
            "result": {
                "protocolVersion": requested or PROTOCOL_VERSION,
                "capabilities": {"tools": {"listChanged": False}},
                "serverInfo": {"name": "mem0", "version": PLUGIN_VERSION},
            },
        }
    if method == "ping":
        return {"jsonrpc": "2.0", "id": request_id, "result": {}}

Sources: integrations/claude-code-plugin/core/mcp_server.py:21-27, integrations/claude-code-plugin/core/mcp_server.py:152-172

JSON-RPC MethodRequest ParametersResponse Structure / Action
initializeprotocolVersion, clientInfoReturns protocol version, tool capabilities, and server info (name: "mem0")
pingNoneReturns empty result object {}
tools/listNoneReturns search_memories schema, description, and annotations
tools/callname, arguments, _metaExecutes memory search or returns ToolInputError / execution failure response
if method == "initialize":
    requested = (message.get("params") or {}).get("protocolVersion")
    return {
        "jsonrpc": "2.0",
        "id": request_id,
        "result": {
            "protocolVersion": requested or PROTOCOL_VERSION,
            "capabilities": {"tools": {"listChanged": False}},
            "serverInfo": {"name": "mem0", "version": PLUGIN_VERSION},
        },
    }
if method == "ping":
    return {"jsonrpc": "2.0", "id": request_id, "result": {}}
if method == "tools/list":
    return {
        "jsonrpc": "2.0",
        "id": request_id,
        "result": {
            "tools": [
                {
                    "name": TOOL_NAME,
                    "description": TOOL_DESCRIPTION,
                    "inputSchema": TOOL_SCHEMA,
                    "annotations": {
                        "readOnlyHint": True,
                        "idempotentHint": True,
                        "openWorldHint": True,
                    },
                }
            ]
        },
    }
…
if method == "tools/call":
    params = message.get("params") or {}
    if params.get("name") != TOOL_NAME:
        result = _tool_response("Unknown Mem0 tool.", is_error=True)
    else:
        try:
            result = _tool_response(
                call_search_memories(params.get("arguments"), _workspace_cwd(params))
            )
        except ToolInputError as exc:
            result = _tool_response(str(exc), is_error=True)
        except Exception:
            result = _tool_response("Memory search failed.", is_error=True)
    return {"jsonrpc": "2.0", "id": request_id, "result": result}

Sources: integrations/claude-code-plugin/core/mcp_server.py:160-191, integrations/claude-code-plugin/core/mcp_server.py:192-205

Warning

When tools/call receives an unknown tool name or encounters an unexpected exception during execution, it returns an explicit tool response dictionary with isError: True rather than raising a protocol-level transport error.

params = message.get("params") or {}
if params.get("name") != TOOL_NAME:
    result = _tool_response("Unknown Mem0 tool.", is_error=True)
else:
    try:
        result = _tool_response(
            call_search_memories(params.get("arguments"), _workspace_cwd(params))
        )
    except ToolInputError as exc:
        result = _tool_response(str(exc), is_error=True)
    except Exception:
        result = _tool_response("Memory search failed.", is_error=True)
return {"jsonrpc": "2.0", "id": request_id, "result": result}

Sources: integrations/claude-code-plugin/core/mcp_server.py:193-205

Tool schema validation and workspace resolution

The tool input schema mandates a query string property up to 2,000 characters and accepts optional parameters including top_k, category, scope, and run_id. The server validates argument types via _validate_arguments, raising ToolInputError if unknown keys are passed or if boundary checks fail. Workspace current working directories are extracted dynamically from turn metadata or environment variables such as CLAUDE_PROJECT_DIR before invoking search_memories.

TOOL_SCHEMA = {
    "type": "object",
    "properties": {
        "query": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000,
            "description": "A direct question about earlier work in this repository.",
        },
        "top_k": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20,
            "description": "Maximum memories to return. Uses Mem0's configured default when omitted.",
        },
        "category": {
            "type": "string",
            "enum": list(CODING_MEMORY_CATEGORY_NAMES),
            "description": "Optional memory category. Omit to search every category.",
        },
        "scope": {
            "type": "string",
            "enum": list(SEARCH_SCOPES),
            "description": (
                "Which memories to search. 'repo' (default) is the whole repository's "
                "shared memory plus your own preferences, 'dir' narrows the shared "
                "part to the current directory, 'mine' is your preferences alone."
            ),
        },
        "run_id": {
            "type": "string",
            "minLength": 1,
            "description": (
                "Optional coding-agent session ID. With any scope, restricts results to memories "
                "saved in that session. Omit to recall memories across sessions."
            ),
        },
    },
    "required": ["query"],
    "additionalProperties": False,
}
…
def _validate_arguments(
    arguments: Any,
) -> tuple[str, int | None, str | None, str | None, str | None]:
    if not isinstance(arguments, dict):
        raise ToolInputError("Search arguments must be an object.")
 
    unknown = set(arguments) - {"query", "top_k", "category", "scope", "run_id"}
    if unknown:
        raise ToolInputError(f"Unknown search argument: {sorted(unknown)[0]}")
 
    query = arguments.get("query")
    if not isinstance(query, str) or not query.strip():
        raise ToolInputError("query must be a non-empty string.")
    query = query.strip()
    if len(query) > 2000:
        raise ToolInputError("query must be at most 2,000 characters.")
 
    top_k = arguments.get("top_k")
    if top_k is not None and (
        isinstance(top_k, bool) or not isinstance(top_k, int) or not 1 <= top_k <= 20
    ):
        raise ToolInputError("top_k must be an integer from 1 to 20.")
 
    category = arguments.get("category")
    if category is not None and category not in CODING_MEMORY_CATEGORY_NAMES:
        raise ToolInputError("category must be one of Mem0's supported categories.")
 
    scope = arguments.get("scope")
    if scope is not None and scope not in SEARCH_SCOPES:
        raise ToolInputError(f"scope must be one of {list(SEARCH_SCOPES)}.")
 
    run_id = arguments.get("run_id")
    if run_id is not None:
        if not isinstance(run_id, str) or not run_id.strip():
            raise ToolInputError("run_id must be a non-empty string.")
        run_id = run_id.strip()
 
    return query, top_k, category, scope, run_id

Sources: integrations/claude-code-plugin/core/mcp_server.py:28-68, integrations/claude-code-plugin/core/mcp_server.py:75-112

Note

The search scope parameter validates against allowed values defined by SEARCH_SCOPES ("repo", "dir", "mine"), while categories must match valid coding memory category names.

"scope": {
    "type": "string",
    "enum": list(SEARCH_SCOPES),
    "description": (
        "Which memories to search. 'repo' (default) is the whole repository's "
        "shared memory plus your own preferences, 'dir' narrows the shared "
        "part to the current directory, 'mine' is your preferences alone."
    ),
},
…
category = arguments.get("category")
if category is not None and category not in CODING_MEMORY_CATEGORY_NAMES:
    raise ToolInputError("category must be one of Mem0's supported categories.")
 
scope = arguments.get("scope")
if scope is not None and scope not in SEARCH_SCOPES:
    raise ToolInputError(f"scope must be one of {list(SEARCH_SCOPES)}.")

Sources: integrations/claude-code-plugin/core/mcp_server.py:48-56, integrations/claude-code-plugin/core/mcp_server.py:98-104

Was this page helpful?