# turbyho/fw-context-mcp [Health: Active]

**Category:** 📟 Embedded System  
**Repository:** https://github.com/turbyho/fw-context-mcp  
**GitHub Stars:** 8  
**Views:** 3  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/turbyho-fw-context-mcp

## Description
Build-aware code intelligence for embedded C/C++ firmware. Indexes your project from compilecommands.json via libclang into SQLite+FTS5. 31 MCP tools for symbol search, call graphs, hotspot analysis, dead code detection, and vector search. For Zephyr, PlatformIO, Mbed OS, Arduino, FreeRTOS.

## Tools
Capabilities this server exposes over MCP:

- **check_dependencies** — Run the full dependency audit. Read-only. Returns structured results.

    Returns the raw per-check dicts (``name``, ``status``, ``message``,
    ``fix_cmd``, ``instructions``, ``critical``) — NOT the formatted
    ``doctor`` table.  Read ``status``/``fix_cmd``/``instructions`` per
    issue; ``status="skipped"`` means a prerequisite is missing.

    Args:
        project_root: Project root directory. Auto-detected from CWD if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.

    Returns:
        list[dict]: one dict per check, ``DepCheckResult`` fields via ``asdict``.
    
- **check_ollama** — Check whether the LLM backend is running and the configured embedding/chat model is installed.

    Read-only: yes. No side effects. Call before smart_search,
    semantic_search, or explain_symbol (when on-demand fallback is
    expected — pre-computed analysis returns instantly without the LLM
    backend).

    Args:
        project_root: Project root. Auto-detected if omitted. Used to
            locate the project's LLM configuration.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.

    Returns:
        dict: {ollama_enabled (bool), status (str — "ok"|"disabled"|
        "not_configured"|"model_missing"|"embedding_unavailable"|"error"),
        ollama_running (bool), ollama_url (str), configured_model (str),
        num_ctx (int), installed_models (list[str]),
        configured_embed_model (str), embedding_installed (bool),
        message (str, on error/disabled), model_details (list[dict], when
        Ollama running), suggest_cloud (bool), vec_available (bool),
        vec_error (str, optional), debug_log (str, optional — only when
        debug logging is enabled)}
    
- **configure_llm** — Configure LLM settings for the current project.

    Writes to ``<project>/.fw-context/local.toml`` ONLY (gitignored,
    per-developer). Does NOT modify the global config or the shared
    project ``config.toml``. After writing, tests the configuration
    by making a simple API call (skipped when LLM is disabled).

    IMPORTANT: When ``chat_api_base`` points to an external host, source
    code snippets in chat prompts will be sent to that endpoint. Ensure
    this complies with your organization's data security policies.
    Consider using local Ollama or an internal API proxy first.

    Args:
        project_root: Project root directory. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        chat_api_base: Chat API URL (see description for format details).
        chat_api_key: Bearer token for cloud/proxy APIs.
        chat_api_format: Override auto-detection: "auto", "ollama", "openai".
        model: Chat model name.
        embed_model: Embedding model name (Ollama only).
        auto_pull: Whether to auto-pull models on 404.
        stream: Stream chat responses via SSE. True avoids reverse-proxy idle timeouts.

    Returns:
        dict: {status ("ok"|"error"), chat_api (dict — configured, endpoint,
        format, model), model (str), auto_pull (bool), stream (bool),
        test_latency_s (float, on success), test_response (str, on success),
        compliance_warning (str, when chat_api_base is external),
        message (str)}
    
- **get_active_build** — MANDATORY FIRST CALL for C/C++ projects. Return metadata about the
    most recently indexed build configuration — check index health before
    using any other fw-context tools.

    Read-only, and it spawns no subprocess — the startup daemon thread and
    the file watcher own the background reindex.

    Act on ``status``:

    * ``"ready"`` — up to date. Continue.
    * ``"reindexing"`` — background reindex running; queries stay accurate.
      Continue. ``reindex_progress`` holds its last log line.
    * ``"reindex_needed"`` — schema mismatch, changed compile_commands.json,
      or a source file that compile_commands.json does not cover. Queries
      still work on existing data. Read ``reindex_reasons``: a missing
      source file needs ``fw-context index --build``, the other two need
      only ``fw-context index``.
    * ``"no_index"`` — initialized, never indexed. Run ``fw-context index``.
    * ``"not_initialized"`` — run ``fw-context init``.
    * ``"error"`` — DB corruption or access error. Use other tools.

    Three conditions set ``reindex_needed``: an outdated schema, a changed
    compile_commands.json, and a source file that is on disk but absent from
    compile_commands.json.  The third one needs a build, because only the
    build system writes that file — a plain reindex has no translation unit
    for the file and skips it without a word.  Modified source files are
    something else: they are handled per-query, and never set it.

    ``indexed_at`` and ``first_indexed_at`` are UTC; file mtimes are local
    time.  Never compare the two directly — in UTC+2 a correctly indexed
    file looks 2 hours newer than ``indexed_at``.  Call with ``fast=False``
    to find modified files.

    ``analysis`` splits the LLM-analysis coverage into project and vendor
    symbols:

    * ``model`` — the model of the analysis, or None. One model only, even
      when several were used.
    * ``analyze_vendor`` — the value at index time, not the current config.
    * ``project`` / ``vendor`` — ``{analyzed, skipped, total}``.
      ``skipped`` = tried, but not analyzable (body larger than the model
      context, an unparseable answer, or a body that was not readable).
    * ``complete`` — no work left: every project symbol is analyzed or
      skipped.  True exactly when ``reindex_reasons`` holds no "unanalyzed
      symbols" entry.  Vendor symbols excluded by ``analyze_vendor=False``
      never block it, thus ``vendor.total`` large with ``vendor.analyzed=0``
      is expected, not a defect.

    Args:
        project_root: Project root directory. Auto-detected from CWD if
            omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        fast: When True (default), the header check reuses the cached
            manifest hashes.  Both modes run the per-file scan, thus
            ``modified_files_count`` is accurate either way — a tool that
            reported "ready" while the search tools warned about the same
            file gave the caller two readings and no way to choose.
            False recomputes the header hashes and costs several times more.

    Returns:
        dict: {config_hash, project_id, project_root, build_system,
        compile_commands, indexed_at (str — "YYYY-MM-DD HH:MM:SS" in UTC,
        the completion time of the last full index), symbol_count, file_count,
        reference_count, modified_files_count (int — files whose content no
        longer matches the index; counted in both modes),
        header_affected_tus (int — number of TUs with stale header
        dependencies), manifest_verification (str —
        "full" when manifest.json exists, "none" otherwise),
        analysis (dict — LLM-analysis coverage split by project/vendor:
        {model, analyze_vendor, project: {analyzed, skipped, total},
        vendor: {analyzed, skipped, total}, complete}),
        description (str), first_indexed_at (str — UTC, same format as
        indexed_at),
        vendor_paths (list[str] — config index.vendor_paths),
        project_paths (list[str] — config index.project_paths),
        bg_reindex_running (bool),
        reindex_progress (str or None — last log line when reindex is running),
        schema_version (int — DB schema version),
        current_schema (int — code expects), status (str — "ready"|"reindexing"|
        "reindex_needed"|"no_index"|"not_initialized"|"error"), reindex_needed (bool —
        structural mismatch requiring a full reindex),
        reindex_reasons (list[str] — why reindex is needed, empty when False.
        One of them asks for `fw-context index --build` rather than a plain
        reindex: when the tree is on a different branch than the index,
        compile_commands.json belongs to the OLD branch and carries its file
        list and its compiler flags, so only a build regenerates it.  Read
        the reason text — it names the command it needs),
        stale (bool — True when reindex_needed or header_affected_tus > 0),
        _warning (str, optional — when manifest verification is not "full"),
        vec_available (bool), vec_error (str, optional),
        index_message (str — human-readable summary of index state),
        multi (bool — True for a multi-variant project),
        variants (list[dict] — {name, description, board}),
        images (list[dict] — {name, description, dir, type}),
        variant_images (dict — variant name to its image names),
        active_variant (str or None — [build] default_variant),
        active_image (str or None — [build] default_image),
        entry_point (str — the `ENTRY()` of the linker script of the build
        that the other fields describe, empty when no script names one),
        memory (list[dict] — the `MEMORY` regions of that build:
        {name, attributes, origin, length, origin_value, length_value,
        file_path, line})}

        About ``memory``: ``origin`` and ``length`` hold the expression the
        script writes, thus they differ by platform — an mbed script writes
        `0xefe00` and a Zephyr script writes `((673792) - 0xe6)`.
        ``origin_value`` and ``length_value`` hold the number, and both are
        None for an expression that names a symbol, such as
        `ORIGIN(RAM) + LENGTH(RAM)`.  The end of a region is
        ``origin_value + length_value``.

        ``memory`` and ``entry_point`` describe ONE build.  For a
        multi-variant project they follow ``config_hash``, which is the
        build named by ``[build] default_variant``, and both are empty when
        the config names no default.  Use ``list_variants`` for the map of
        every build.

        ``memory`` is empty for a build system that records no linker
        script.  A PlatformIO project is the measured case: SCons writes no
        ninja file and no link command the index can read, and the map file
        never names the script.  An empty list means "not recorded", never
        "no memory".

        ``defines`` (dict — the `-D` flags of that build) and
        ``defines_varying`` (int).  ``defines`` holds only the names that
        EVERY translation unit of the build carries with the same value, so
        the tool never shows the defines of one file as the defines of the
        build.  ``defines_varying`` counts the names left out, thus a name
        absent from ``defines`` is either not defined at all or not defined
        everywhere — measured on the Mbed project: 27 names in all 881 units, 59
        in only some, where the three assembly files get a shorter set.

        This is the configuration the BUILD states, not every macro the
        preprocessor saw.  The second is three orders of magnitude larger —
        27800 distinct names on the STM32 project — and almost all of it comes from the
        headers and the compiler.  A Zephyr build keeps its real
        configuration in ``autoconf.h`` (740 `CONFIG_*` names) and passes
        few `-D` flags, so ``defines`` says little there and a great deal on
        an mbed build, where it holds `APPLICATION_ADDR`,
        `APPLICATION_SIZE`, and `CMSIS_VECTAB_VIRTUAL`.

        For a project that is not initialized, the result holds only
        ``status``, ``project_root``, and ``index_message``.  When no index
        exists, the result adds ``project_id``.
    
- **get_environment_status** — Return the complete project environment status in one call.

    Read-only. Aggregates five domains into a single call so the LLM can
    see everything at session start without extra round-trips:

    - ``deps`` — dependency audit (``run_full_check``), each entry with an
      optional ``action`` (``message`` + shell ``command``).  ``status="skipped"``
      means a prerequisite is missing (e.g. ``libclang-so`` skipped because
      ``libclang-python`` is absent) — not a failure.
    - ``build_system`` — detected build system, ``None`` when unknown.
    - ``compile_db`` — whether compile_commands.json exists and its entry count.
      Reported as ``{"exists": false, ...}`` before init (no config to resolve
      the path from, and loading one would create empty config files).
    - ``index`` — the FULL ``get_active_build()`` result, unchanged (its action
      lives in ``index_message``).
    - ``llm`` — LLM backend status with an optional ``action``.

    When the project is not initialized (``index.status == "not_initialized"``),
    only the config-independent dependency subset runs (checks that do not need
    a project config) — Ollama/model/db/build checks are skipped.

    Args:
        project_root: Project root directory. Auto-detected from CWD if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.

    Returns:
        dict: {init_status (str — "initialized" or "not_initialized"),
        deps (list[dict] — name, status, message, and an optional action),
        build_system (str or None),
        compile_db (dict — {exists (bool), path (str or None),
        entry_count (int or None — None before init, and when fw-context
        cannot read the file)}),
        index (dict — the full ``get_active_build`` result),
        llm (dict — {enabled, ollama_running, chat_model, embed_model}, plus
        ``ollama_enabled`` when the LLM check ran, plus an optional
        ``action``)}.
    
- **get_project_info** — Return project metadata (name, type, root_path) for a project ID.

    Looks up the global project registry at ``~/.fw-context/projects.db``.
    Use this to identify a project from its UUID4 — find out what build
    system it uses, its name, and where it was last indexed.

    Read-only. No side effects.

    Args:
        project_id: Project ID (UUID4 hex) to look up.

    Returns:
        dict: {project_id, name, project_type, root_path, created_at, updated_at}
        or {"error": "..."} when the project_id is not registered.

        On failure the dict holds only ``error`` with the reason.
    
- **list_projects** — List all indexed firmware projects with their statistics.

    Read-only. No side effects. Use at session start to discover available
    projects; use ``get_active_build`` for details on the currently active project.

    ``indexed_at`` and ``first_indexed_at`` are UTC, in ``"YYYY-MM-DD
    HH:MM:SS"`` format — the same format that ``get_active_build`` returns.

    ``analysis`` holds the ``project`` and ``vendor`` counts only.  For the
    ``model``, ``analyze_vendor``, and ``complete`` fields, call
    ``get_active_build`` for that project.

    Args:
        project_root: Project root. Auto-detected if omitted. Pass to
            distinguish multiple indexed projects.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.

    Returns:
        list of dicts, each with: project_id, name, root_path, build_system,
        symbol_count, file_count, indexed_at (str — UTC), description (str),
        first_indexed_at (str — UTC), schema_version, current_schema,
        reindex_needed (bool), status (str — "ready" or "reindex_needed"),
        db (path to SQLite database file),
        variant_count (int — number of build variants),
        image_count (int — number of sysbuild images),
        analysis (dict — LLM-analysis coverage
        {project: {analyzed, skipped, total},
        vendor: {analyzed, skipped, total}}, or None when no build is
        indexed).

        When no project has an index, the result is a single dict with an
        ``info`` key.  When fw-context cannot read a database, the result
        holds a dict with ``db`` and ``error`` keys for that file.
    
- **list_variants** — List every indexed build with its (variant, image, board) identity.

    Read-only diagnostic — shows what is actually indexed, not what the config
    declares.  Each row is one ``(variant, image)`` build with its own
    ``config_hash`` and symbol count.  For single-project indexes this returns
    one row with ``variant``/``image`` empty.

    Use ``get_active_build`` for the mandatory first-call health check and the
    human-readable ``variants``/``images`` discovery; use this tool to see the
    per-build ``config_hash`` and symbol counts (authoritative per-build state).

    Args:
        project_root: Project root directory. Auto-detected from CWD if
            omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.

    Returns:
        dict: {builds (list[dict]), multi (bool — True when the config
        declares variants or a build has a non-empty variant name)}.

        Each build dict holds: variant (str — empty for a single-project
        index), image (str — empty for a single-project index), board (str),
        config_hash (str), symbol_count (int), file_count (int),
        manifest_verification (str — "full" or "none"),
        entry_point (str — the `ENTRY()` of the linker script of this build,
        empty when no script names one),
        memory (list[dict] — the `MEMORY` regions of this build:
        {name, attributes, origin, length, origin_value, length_value,
        file_path, line}).  `origin` and `length` are the expression the
        script writes; `origin_value` and `length_value` are numbers, and
        they are None for an expression that names a symbol such as
        `ORIGIN(RAM) + LENGTH(RAM)`.  Empty for a build whose system
        records no linker script — see the note below.

        THIS is where a per-build memory map lives, not in the `images`
        list of ``get_active_build``: that list holds one entry per image
        NAME, and one name can belong to two variants with different
        addresses.

        When the project is not initialized, or has no index, the result is
        {builds: [], multi: False, error (str)}.
    
- **reindex_file** — Re-parse a single source file with libclang and update its symbols in the index.

    Not read-only — uses the exact compiler flags from ``compile_commands.json``.
    Use after editing a file to keep the index current without a full rebuild.

    A source file must be listed in ``compile_commands.json``.  A header is
    not listed there, thus it is re-parsed through one unit that includes
    it.  That answer covers a single compilation context, thus the result
    carries a ``warning`` — only a full ``fw-context index`` covers every
    unit that includes the header.

    Also regenerates LLM analysis and method override relationships for
    affected symbols when those features are enabled in config.  An
    unchanged symbol keeps its stored analysis.

    Args:
        file_path: Path to the file to re-parse.  A source file must be in
            compile_commands.json; a header goes through one including unit.
        project_root: Project root directory. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.

    Returns:
        dict: {file, translation_units, symbols_updated, elapsed_s,
        analysis_updated (if LLM enabled), or error}.
    
- **reindex_file_impl** — Re-parse a single source file with libclang and update its symbols in the index.

    Not read-only — uses the exact compiler flags from ``compile_commands.json``.
    Use after editing a file to keep the index current without a full rebuild.

    A source file must be listed in ``compile_commands.json``.  A header is
    not listed there — compile_commands.json names translation units — so it
    is re-parsed through one unit that includes it, taken from the manifest.
    That answer describes a single compilation context, thus the result
    carries a ``warning``: another unit can see the header under a different
    set of ``#define`` values and still hold stale symbols.  Only a full
    ``fw-context index`` covers every context.  One unit and not all of them
    is a cost decision — an application header reaches a median of 3 units
    but as many as 266 on a real project, at tens of seconds each.

    Also regenerates LLM analysis and method override relationships for
    affected symbols when ``with_analysis=True``.  The analysis is
    content-addressed, thus an unchanged symbol is never re-analysed.

    Args:
        file_path: Path to the file to re-parse.  A source file must be in
            compile_commands.json; a header goes through one including unit.
        project_root: Project root directory. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        with_analysis: When True (default), also regenerates LLM symbol analysis,
            method override relationships, PageRank, and embeddings. Set False
            for a fast symbol-only update (used by background auto-reindex).

    Returns:
        dict: {file, translation_units, symbols_updated, elapsed_s,
        analysis_updated (if LLM enabled with analysis), or error}.

        On failure the dict holds only ``error`` with the reason.
    
- **reset_index** — Delete the entire symbol index for a project.

    Not read-only — permanently deletes the SQLite database and WAL files.
    Call with ``confirm=False`` first (dry-run) to see what would be deleted.
    Re-index with ``fw-context index`` afterwards.

    Handles corrupt databases gracefully — you can delete a corrupt index
    without needing to open it first.

    Args:
        project_root: Project root directory. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        confirm: Must be True to execute. Call without first as dry-run.

    Returns:
        dict: {project_root, db, project_id, action: "dry_run"|"deleted",
        message, symbol_count, indexed_at (dry-run)}.

        A ``warning`` key means that the database is corrupt — the
        integrity check failed, thus the counts can be incomplete.

        On failure the dict holds only ``error`` with the reason.
    
- **lookup_symbol** — Look up a C/C++ symbol by name via libclang index — exact or prefix
    matching. Finds symbols text-based search can miss: build-conditional
    code, template instantiations, macro-expanded names. Macros are
    extracted via ``clang -dM -E`` during indexing so ``#ifdef``-conditional
    macros resolve correctly for the active build config. Prefer this over
    search_code when you know the exact symbol name or a prefix
    (``uart_`` finds all UART symbols). Use search_code for
    keyword/concept search.

    Read-only: yes. May auto-reindex stale files (non-blocking).

    Args:
        name: Symbol name (exact match) or prefix (set exact=False).
            E.g. 'uart_init' finds the exact function; 'uart_' finds
            all symbols starting with 'uart_'.
        project_root: Project directory. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        exact: True = exact name match, False = prefix LIKE match (default).
        limit: Maximum results (default 50).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list[dict]: Symbols with name, qualified_name, kind, file, line,
        signature, docstring, is_definition, is_template, is_virtual,
        is_pure_virtual fields. Enum constants include ``enum_value``
        with the integer value. Macro results include ``kind="macro"``,
        ``value`` (raw definition), and ``expanded_value`` (preprocessor-
        resolved value). May also include ``template_usr``,
        ``parent_usr``, and ``llm_analysis`` (``{summary, inputs,
        outputs}``) when available.  A model wrote the text in
        ``llm_analysis``, and the code did not — use it to find a symbol,
        and quote ``signature``, ``docstring``, or ``get_source`` instead.
        When no results found, may include ``_did_you_mean`` with suggested
        symbol names. When no symbol matches, the list is empty —
        this tool gives no ``info`` entry for an empty result.

        **Note:** C++ constructors share their name with the enclosing
        class, so ``lookup_symbol("Foo")`` may return both ``class Foo``
        and ``constructor Foo::Foo()``.  Use the ``kind`` field to
        filter when you need a specific symbol type.

        A symbol that comes from the relaxed prefix fallback carries
        ``_fallback: True`` — the name is not an exact match of *name*.

        A list with one dict that holds an ``error`` key means that the
        project has no index, or that the lookup failed.  Read that key
        before you read the result fields.
    
- **search_code** — Find C/C++ symbols by name — searches function/class/enum NAMES.

    Searches symbol names, qualified names, signatures, docstrings, and
    pre-computed name tokens (CamelCase/snake_case split).  Does NOT search
    function bodies — for patterns in code like ``.attach(``,
    interrupt handler registrations, callback attachments use ``search_bodies`` instead.

    Use when you know the concept but not the exact name
    (``"interrupt handler"``, ``"modem init"``).  Prefer ``lookup_symbol``
    when you already know the exact or prefix name.

    Results hold the metadata of each symbol — name, location, signature,
    docstring — not its implementation code.

    **FTS5 syntax:**
    - Every bare term gets a trailing ``*`` and the terms are OR-joined:
      ``modem init`` goes to FTS5 as ``modem* OR init*`` and answers with
      the symbols that hold EITHER word.  ``search_bodies`` does the
      opposite — it takes the query literally, where a space is an AND.
    - ``init*`` matches init, init_uart, initialize (trailing wildcard)
    - ``"spi init"`` matches the exact phrase "spi init"
    - Do NOT use underscore in queries — ``modem_init`` is split into
      ``modem AND init``. Write ``modem init`` instead.
    - Punctuation is not searchable.  The tokenizer drops it, thus
      ``.attach(`` becomes a phrase that looks for the token ``attach``.
      The query is repaired, never rejected.

    **Progressive relaxation:** when FTS5 finds nothing, the search widens
    in up to six steps, and every result carries the ``_fallback`` method
    that found it:

    1. FTS5 with the ``kind`` filter — ``_fallback="fts5"``.
    2. FTS5 without it; operators often guess the wrong kind.
    3. ``name_tokens`` substring match over the pre-computed CamelCase /
       snake_case tokens (``BuildType`` is indexed as ``"build type"``).
       Needs N−1 of N query terms — ``"name_tokens_like"``.
    4. LIKE over the docstring column, for a single-term query that the
       token steps missed — ``"docstring_like"``.
    5. FTS5 per query word, results merged — ``"individual_terms"``.
    6. ``macros_fts`` for ``#define`` names and values, kind="macro" —
       ``"macros_fts"``.

    **Kind filter values:** ``function``, ``method``, ``constructor``,
    ``destructor``, ``class``, ``struct``, ``union``, ``enum``, ``enum_constant``,
    ``typedef``, ``varglobal``, ``varlocal``, ``variable``, ``field``,
    ``namespace``.

    **Local variables are out.**  FTS5 indexes the qualified name, thus a
    local matches through the function that holds it: a query for
    ``sensor`` used to answer with ``V``, ``ret`` and ``tmp_value`` from
    inside ``read_sensor_value``, 4 of 20 results on one measured query.
    A local is never the answer to "which symbol is about X", thus
    ``varlocal`` and the legacy ``variable`` kind are excluded.
    ``varglobal`` stays — a global carries architectural weight.  Ask for
    them explicitly with ``kind="varlocal"``, or use ``find_variables``.

    After ``fw-context index --analyze``, a result also holds
    ``llm_analysis`` — ``{summary, inputs, outputs}``.  A model wrote that
    text, and the code did not.  Treat it as a hint that points you at a
    symbol, never as a fact to quote.  Quote ``source`` from
    ``get_source``, ``signature``, or ``docstring``.

    Read-only. No side effects.

    Args:
        query: FTS5 search terms. Keep queries short — 1–3 words.
        project_root: Project root directory. Auto-detected from CWD if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        kind: Optional filter to return only symbols of this kind.
        limit: Maximum results (default 20, max 100).
        project_only: When True, exclude vendor SDK directories and return only
            application code. Default False.
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts, each with: name, qualified_name, kind, file, line,
        is_definition, signature, docstring, is_template, is_virtual,
        is_pure_virtual. Enum constants include ``enum_value`` with the
        integer value. May also include ``template_usr``, ``parent_usr``,
        and ``llm_analysis`` (``{summary, inputs, outputs}`` — written by a
        model, not by the code.  ``get_active_build().analysis.model`` names
        it). Fallback results include ``_fallback`` with the method name.

        No match gives ``[]``.  A dict with ``error`` means the query
        failed.  A stale index prepends a dict with ``warning`` + ``hint``.
    
- **search_bodies** — Find patterns in the TEXT OF A DEFINITION — the code inside its extent.

    Searches the stored text of every definition (``is_definition=1``), and
    a definition is not only a callable.  Measured on one project of 60,877
    symbols, the text covers:

    - Callables — ``function``, ``method``, ``constructor``, ``destructor``.
      Call patterns (``.attach(``, ``.rise(``, ``callback(&``), ISR
      registration, one ``case`` label of a long ``switch``.
    - Types — ``class``, ``struct``, ``union``, ``enum``, ``namespace``.
      An enum constant, a bit field, a member declaration such as
      ``InterruptIn _pin;`` — all inside the body of the type that holds
      them.
    - Definitions of data — ``varglobal``, ``varlocal``, ``typedef``.  A
      table with a multi-line initializer is found by its content.

    A match on a type reports the type as the result, thus a query for one
    enum constant answers with the enum, and ``match_lines`` gives the line
    of the constant itself.

    **Only the text matches.**  The query is bound to the stored body: a
    hit in the NAME, the signature, the docstring or the ``llm_analysis``
    of a symbol is not a hit here.  Measured on one project, ``sensor``
    used to give 36 results of which 22 matched only through a summary that
    a model wrote — untrusted text that cannot be cited, and a
    ``_match_snippet`` with no match in it.  Use ``search_code`` to reach a
    name or a concept.  A column filter you write yourself
    (``summary : sensor``) overrides the binding.

    **When to use ``search_bodies`` and when ``search_code``:**

    - ``search_bodies`` — patterns in the code (what the code DOES or
      DECLARES): ``self test``, ``attach``, ``SELF_TEST``.
    - ``search_code`` — symbols by NAME (what the code IS): ``modem init``,
      ``interrupt handler``.

    **The query goes to FTS5 as you wrote it.**  This tool alone adds no
    wildcard, and that is what keeps a pattern precise:

    - A space is an AND of two exact tokens, NOT an OR.  ``CommandType NUM``
      answers with the definitions that hold both.
    - No prefix is implied.  ``SELF_TEST`` matches the tokens ``self test``
      and misses ``Self tester``; write ``SELF_TEST*`` to reach the second.
      Measured on one project, the wildcard added the one caller that the
      bare query missed.
    - Punctuation is not searchable.  FTS5 cannot parse ``.attach(`` at
      all, thus the query is repaired into the phrase ``".attach("`` — and
      the tokenizer inside a phrase drops the punctuation too, so what runs
      is the word ``attach``.  Such a result carries ``_fallback:
      "sanitized"`` and ``_query_used``.  The hits whose body really holds
      ``.attach(`` are the ones with ``match_lines``.
    - ``search_code`` and ``search_content`` behave the OTHER way: each of
      their terms gets a trailing ``*`` and the terms are OR-joined.

    **Limitation — the extent of a definition is the boundary.**  Text that
    belongs to no definition is out of reach:

    - ``#include``, ``#define``, ``#ifdef`` — preprocessor directives.
      ``search_code`` covers a macro name and value.  ``search_content``
      covers the directive as text.
    - ``extern "C"`` — a linkage specifier is no symbol.
    - A comment or a declaration at file scope, outside every definition.

    For those, use ``search_content``, which indexes the full file text.

    Set ``project_only=True`` for a question about YOUR code (``"where do we
    register interrupt handlers?"``).  Leave it ``False`` (default) when the
    vendor SDK code — the framework or OS code that your team did not write
    — is also relevant.

    Results include ``_match_snippet`` — a highlighted excerpt that shows
    each match in context (e.g. ``_timeout.<b>attach</b>(callback(...))``) —
    and ``match_lines``, the line numbers of the matches inside the
    definition.  ``line`` is where the definition starts, which for a large
    function is far from the match.  Cite from ``match_lines`` instead.
    Project code sorts before vendor code in the output.

    Read-only. No side effects. Requires the FTS5 index.

    Args:
        query: FTS5 search terms, 1-3 words.  A bare multi-word query is an
            AND of exact tokens, and no wildcard is added — see the query
            rules above.  A single word is the broadest form: ``'attach'``
            reaches every ``.attach(...)`` pattern.  Add ``*`` for a prefix
            (``'attach*'``), and double quotes for a phrase
            (``'"attach callback"'``).
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        kind: Optional filter to return only symbols of this kind.
        limit: Maximum results (default 20, max 100).
        project_only: When True, exclude vendor SDK directories and return only
            application code. Default False.
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts, each with: name, qualified_name, kind, file, line
        (first line of the definition), is_definition, signature,
        _match_snippet (excerpt around the match), source (the text of the
        definition).

        Also, when they carry an answer:

        * ``match_lines`` (list[int]) — absolute line numbers of the
          matches, up to 20.  Computed from the full text, thus a match
          after the cut below still has a number.  Use these to cite
          ``file:line``, and not the ``line`` of the definition.  The name
          carries no leading underscore for a reason: a field the caller
          must cite is an answer, while ``_``-prefixed fields
          (``_match_snippet``, ``_fallback``, ``_source_truncated``) tell
          where the answer came from.
        * ``_source_truncated`` (True) — ``source`` is cut.  A callable
          keeps 2000 characters, any other kind 500, because the body of a
          type is mostly members that the match has nothing to do with.
          ``get_source`` gives the whole text.
        * ``_fallback`` (``"sanitized"``) with ``_query_used`` — FTS5 could
          not parse the query as written, thus a repaired one ran.  The
          repair drops punctuation, so the answer is wider than the text
          that was asked for.  Every query FTS5 accepts runs untouched and
          carries neither field.

        ``source`` here is bare text with no line-number prefix.  Only
        ``get_source`` numbers its lines.

        No match gives ``[]``.  A dict with ``error`` means the query
        failed.  A stale index prepends a dict with ``warning`` + ``hint``,
        and so does a query that FTS5 refuses to parse — an empty list
        always means "no such code", never "bad query".
    
- **search_content** — Find patterns in FULL file content — the whole file, not only the
    text that belongs to a definition.

    Searches **ifdef-filtered** file text — only code that actually compiles
    for the current build configuration.  Inactive ``#ifdef`` branches are
    replaced with blank lines (preserving original line numbers).

    Covers the text that belongs to no definition, which is what
    ``search_bodies`` cannot see: ``#include``, ``#define``, ``#ifdef``,
    ``extern "C"``, and a comment or declaration at file scope.  It covers
    the text of definitions too.  To find a symbol by NAME (``modem init``,
    ``interrupt handler``), use ``search_code``.

    **Not a fallback of ``search_bodies`` — its complement.**  The two
    answer different questions and reach different text:

    - ``search_bodies`` answers WHICH DEFINITION holds the pattern, and
      takes the query literally (no wildcard, space = AND).
    - ``search_content`` answers WHICH FILES the topic touches, and widens
      the query: every term gets a trailing ``*`` and the terms are
      OR-joined.  The wider query reaches text the literal one misses —
      measured on one project, ``SELF_TEST`` found 6 files here and the
      same word found 5 through ``search_bodies``, the extra file holding
      the comment ``Self tester``.

    For the footprint of one feature, run both.

    Results are file-level — one entry per matching file, with
    ``match_lines`` for the lines that hold a query term.
    ``project_only=True`` filters to ``is_project = 1`` files; the default
    False includes the vendor SDK files.

    When ``files_fts`` is missing (legacy index), falls back to LIKE
    search on ``files.content`` — results include ``_fallback: "like"``
    and no snippet highlighting. Run ``fw-context index`` to upgrade.

    Read-only. No side effects. Requires the FTS5 index with file content.

    Args:
        query: FTS5 search terms. 1-3 words. Bare multi-word queries are
            OR-joined (prefix-wildcarded). Prefer single-word queries.
            E.g. ``'InterruptIn'``, ``'extern C'``, ``'#define'``.
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        limit: Maximum results (default 20, max 100).
        project_only: When True, filter to project code only (files with is_project = 1).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts, each with: file, language, mtime,
        _match_snippet (highlighted excerpt around the match).

        Also, when it carries an answer:

        * ``match_lines`` (list[int]) — line numbers of the lines that hold
          a query term, up to 20.  They are the line numbers of the file
          itself: an inactive ``#ifdef`` branch is a blank line, thus the
          count never shifts.  Cite ``file:line`` from here.

          The field is absent when FTS5 matched a variant of the token that
          the term is not a substring of — ``SELF_TEST`` matches the file
          that writes ``Self tester``, and no line holds ``self_test``.
          Read ``_match_snippet`` in that case.

        No match gives ``[]``.  A dict with ``error`` means the query
        failed.  A stale index prepends a dict with ``warning`` + ``hint``,
        and so does a query that FTS5 refuses to parse — the answer then
        comes from the LIKE path and carries ``_fallback: "like"``.
    
- **semantic_search** — Semantic search using pre-computed libclang symbol embeddings. Finds
    symbols by meaning, not by text — matches concepts even when query
    words don't appear literally in the code. Uses cosine similarity over
    variable-dimension embeddings generated during ``fw-context index``.
    Dimensions vary by model: mxbai-embed-large → 1024,
    qwen3-embedding → 4096.

    **When to prefer over search_code:** When you're describing a *concept*
    rather than searching for a known keyword.  Examples:
    - ``"parcel locker state"`` finds door-state and shipment methods even
      though "parcel" and "locker" don't appear in their names.
    - ``"cell modem"`` finds ``_socket_t`` and ``ModemMsg*`` classes.
    - ``"delivery box"`` finds ``set_shipment`` and ``get_zrtdata``.
    - ``"power consumption"`` finds ``get_load_power`` and INA260 class.

    **When to prefer search_code instead:** When you know the exact keyword
    or symbol name (``"fram_write"``, ``"cbor encode"``).  FTS5 is faster
    and more precise for lexical matches.

    **Threshold guidance (mxbai-embed-large model):**
    - ``0.50`` — exploratory: more results, lower precision
    - ``0.55`` — balanced (~1000 results)
    - ``0.60`` — precise: ~175 avg, high precision (default)
    - ``0.65`` — strict: few results, may miss relevant symbols

    **Source-aware ranking:** Project code boosted 1.2×, library code
    1.1×, vendored SDK code 0.85×.

    **Requires an LLM** with an embedding model.
    Falls back to ``search_code`` with a warning if the LLM is unavailable.

    Read-only. No side effects.

    Args:
        query: Natural language description of what you're looking for.
               Be specific — 5–15 words works best.
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        threshold: Minimum cosine similarity (0.0-1.0). Default 0.60.
        limit: Maximum number of results (default 20, max 100).

    Returns:
        list of dicts, each with: name, qualified_name, kind, file, line,
        is_definition, signature, docstring, plus ``_similarity`` (cosine
        similarity score) and ``_method`` (``"embedding"`` or
        ``"search_code_fallback"``).

        When the best similarity is below the relevance floor (0.68), the
        result is one dict with ``warning``, ``_best_similarity`` (float),
        ``_fallback_suggestion`` (``"search_code"``), and ``_results`` (the
        low-similarity results).  Treat those results as noise, and use
        ``search_code`` instead.

        When the LLM is not running, or the embedding fails, this tool falls
        back to ``search_code``.  The results then carry
        ``_method: "search_code_fallback"``, and a leading dict holds a
        ``warning`` with the reason.

        No match gives ``[]``.  One dict with ``error`` means the query
        failed — check that key first.
    
- **smart_search** — Natural-language search: an LLM generates FTS5 keywords, then searches
    the libclang index. Finds concepts by meaning rather than exact text
    match. Prefer this when you don't know the exact keywords and want to
    describe what you're looking for ("how does the modem connect?",
    "handle BLE pairing failure").

    Read-only. No side effects. Slow (10-30 s) — delegates to the full
    ``SMART_SEARCH`` pipeline (translate → rough_search → llm_query →
    fts5_search → refine → embedding → adaptive_fusion → deduplicate →
    expand_context → format).

    Multi-phase approach:
    1) Translate non-English queries
    2) Rough search to gather sample symbols for naming conventions
    3) LLM sees those samples + query and generates FTS5 terms
    4) FTS5 search with generated terms
    5) Refine: LLM checks results and course-corrects query terms
    6) Semantic embedding search (cosine similarity re-rank)
    7) Deduplicate, score, and format results

    **When to prefer over search_code:** When you don't know the exact keywords
    and want to describe what you're looking for ("how does the modem connect?",
    "handle BLE pairing failure").

    **Fallback:** When LLM is unavailable, falls back to direct FTS5 search
    with word-split terms from the query.

    Args:
        query: Natural language description of what you're looking for.
               Be specific — 5–15 words works best.
        project_root: Project root directory. Auto-detected from CWD if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        limit: Maximum number of results (default 20, max 100).

    Returns:
        list of dicts with metadata entries (_generated_queries, _rough_queries,
        _translated_from) followed by symbol results with name, qualified_name,
        kind, file, line, is_definition, signature, docstring.

        When the LLM stalls, the tool gives the FTS5 results that it has.
        The leading dict then holds ``_partial: True``, a ``warning`` with
        the timeout, and a ``hint``.  The result is incomplete: make the
        query more specific, or increase the LLM timeout.

        When the index is stale, a leading dict holds a ``warning`` and a
        ``hint`` to reindex.

        No match gives ``[]``.  One dict with ``error`` means the query
        failed — check that key first.
    
- **find_all_callers_recursive** — Find all transitive C/C++ callers — who calls *name*, directly or
    indirectly, through the libclang call graph including
    function-pointer edges, implicit constructors, and synthetic
    dispatch edges. libclang-powered: follows function-pointer
    assignments and ISR vector registrations across the full call tree.

    Use for impact analysis: "if I change this function, how far does the
    ripple go?"  Returns callers at depth 1 (direct), depth 2 (callers of
    callers), up to ``max_depth`` (default 5).  Results are deduplicated —
    each caller appears once at its shortest distance to the target.

    **Edge types traversed:** Includes ``call``, ``indirect`` (function
    pointers / ISRs), ``implicit_construct`` (constructors reachable through
    file-scope global objects), and ``dispatch`` (synthetic edges through
    event loops and thread starts).

    **Limitation — ambiguous name resolution:** When a source-line fallback
    cannot disambiguate which method is called (e.g. ``attach()`` matching
    both ``Timeout::attach`` and ``SerialBase::attach``), the edge is
    conservatively omitted to avoid false callers.  If you suspect a
    missing caller, verify with ``search_bodies("target_name")`` and
    ``find_indirect_targets``.

    For a flat, single-level caller list use ``find_callers`` (faster).
    For the reverse direction use ``find_callees_recursive``.

    Read-only. No side effects. Requires the reference index
    (``fw-context index`` — refs on by default). BFS from the target
    outward; performance scales with call-graph fan-out.

    Args:
        name: Symbol name to find transitive callers of.
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        max_depth: Maximum BFS depth for transitive search (default 5).
        limit: Maximum results (default 50).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts, each with: caller (str — caller name),
        caller_qualified_name (str), depth (int — distance from target),
        file (str), line (int), ref_kind (``"call"`` or ``"indirect"``).

        Never empty: one dict with ``error`` (cannot resolve) or ``info``
        (no results) replaces the results.  Check both keys first.
    
- **find_call_path** — Find call paths between two C/C++ functions via BFS in the libclang
    call graph, including function-pointer edges, ISR vector
    registrations, implicit constructors, and synthetic dispatch edges
    (event loops, thread starts).  libclang-powered: follows
    function-pointer edges and ISR vector registrations that text-based
    search cannot resolve.

    Use to answer "how does A reach B?" — e.g. tracing how a high-level
    event handler eventually calls a low-level driver.  Returns up to 5
    shortest paths, each with ``depth`` (edge count) and ``chain``
    (e.g. ``"main → app_run → modem_init"``).

    **Edge types traversed:** The BFS includes ``call``, ``indirect``
    (function pointers / ISRs), ``implicit_construct`` (global/static
    object constructors), and ``dispatch`` (synthetic edges through event
    loops like ``EventQueue::dispatch_forever`` and thread starts like
    ``Thread::start``).

    **Limitations:**

    - **Dispatch bridges:** callbacks registered through
      ``EventQueue::call_every``, ``k_work_submit``, or ``xTimerStart``
      reach their dispatch entry point (``dispatch_forever``,
      ``z_work_q_main``) through a built-in map for mbed-os, Zephyr, and
      FreeRTOS.  Add other RTOS patterns in
      ``[call_graph.dispatch_bridges]`` (``.fw-context/config.toml``); a
      bridge whose entry symbol is not in the index is skipped silently.
    - **Ambiguous fallback names:** for a call that libclang cannot
      resolve (template-obscured ``_timeout.attach(...)``), a source-line
      regex matches the method name.  When several methods share that
      unqualified name and neither the receiver field type nor the caller
      class disambiguates, fw-context creates NO edge — conservative, to
      avoid false paths.
    - **Global constructors:** file-scope ``implicit_construct`` edges
      hang off a synthetic ``<global ctors>`` node between ``main`` and
      every global constructor.  Any query that can reach ``main`` uses
      it, not only a query that starts at ``main``.

    **On an empty result** that you expected to hold a path: look for async
    dispatch (``search_bodies("call_every")``, ``search_bodies("attach")``),
    trace the intermediate symbols with ``find_callers``, raise
    ``max_depth``, and check the function-pointer wiring with
    ``find_indirect_call_sites`` / ``find_indirect_targets``.

    For one-sided exploration use ``find_all_callers_recursive`` (who reaches
    this?) or ``find_callees_recursive`` (what does this reach?).
    For exact call-graph verification use ``find_callers`` or
    ``find_references``.

    Read-only. No side effects. Requires both symbols to be in the index
    and refs enabled (``fw-context index`` — refs on by default).

    Args:
        from_name: Starting symbol for path search.
        to_name: Target symbol to find path to.
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        max_depth: Maximum BFS depth for path search (default 10, max 50).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts, each with: depth (edge count, int), chain (str —
        e.g. ``"main → app_run → modem_init"``). When no path exists
        within the depth limit, the list holds one ``info`` dict.

        Never empty: one dict with ``error`` (cannot resolve) or ``info``
        (no results) replaces the results.  Check both keys first.
    
- **get_vector_table** — Read the interrupt vector table, and say what services each interrupt.

    The vector table is how an interrupt reaches code.  Nothing CALLS a
    handler — the hardware reads a slot and jumps — so a handler has no
    caller, and every other tool shows it as unreferenced.  This tool
    reads the table itself, from the assembly the build compiles.

    Use it to answer "which interrupts does this firmware service", to
    find the handler for one interrupt, or to find the interrupts that
    reach the trap loop.

    **The slot number is the position in the table.**  What that position
    means belongs to the architecture, not to the index.  On Cortex-M
    slots 0 to 15 are the system exceptions and slot 16 + n is external
    interrupt n, so ``TIM2_IRQHandler`` in slot 44 is ``TIM2_IRQn = 28``.
    On other architectures the same position means something else.

    The ``status`` field says what services the interrupt:

    * ``"c"`` — a definition outside assembly.  Code runs.  When the
      index also holds the weak definition that this one replaced, the
      row has ``overridden`` with its file and line.
    * ``"assembly"`` — a strong assembly definition.  Assembly services
      the interrupt.
    * ``"unhandled"`` — a weak assembly definition that nothing
      overrode.  A CMSIS startup file makes this an alias of
      ``Default_Handler``, which is an infinite loop.  If the interrupt
      fires, the device stops.
    * ``"runtime"`` — the image holds that same alias, and the code
      installs a real handler into this slot by calling
      ``NVIC_SetVector``.  A target that defines
      ``CMSIS_VECTAB_VIRTUAL`` keeps its vector table in RAM and fills
      it that way, so the interrupt IS serviced once the registering
      code has run — and not before it.  The row holds ``installed``,
      one entry per call site with the handler name, its file and line,
      and ``at``, where the registration happens.  Follow ``at`` to see
      WHEN it happens: on the Mbed project ``us_ticker_irq_handler``
      reaches slot 25 from ``us_ticker_init``, so the tick source is
      unserviced until the ticker starts.  A row with a real static
      definition keeps its own status and still carries ``installed``.
    * ``"data"`` — the slot holds an address BUILT from the symbol it
      names (``.word z_main_stack + CONFIG_MAIN_STACK_SIZE``), so the
      symbol is a base and nothing jumps to it.  On Cortex-M this is
      slot 0 of a Zephyr table: the initial stack pointer.  Do not read
      it as code.
    * ``"linker"`` — the linker script gives the address and no compiled
      file defines the name.  Slot 0 holds the initial stack pointer, not
      a handler, and looks like this.  When the index read the script,
      ``file`` and ``line`` name the assignment in it — on the Mbed project,
      `__StackTop` at `.link_script.ld:148`.  Do not read this row as
      code: there is no function to follow.
    * ``"dispatcher"`` — the slot reaches a function that holds more than
      one slot of this table AND calls through a pointer.  It cannot be
      servicing one particular interrupt; it decides at run time where to
      go.  Zephyr fills every external IRQ slot with ``_isr_wrapper``,
      which reads the interrupt number and jumps through
      ``_sw_isr_table``.  Follow it: ``get_symbol_context`` on the name,
      then ``find_references`` on the table it uses.  A handler that
      merely calls one registered callback is NOT this — it holds a
      single slot and keeps ``"c"``.

    A ``"c"`` row with ``overridden`` is the CMSIS pattern: the startup
    file defines each handler weakly, the project defines the same name
    again, and the linker keeps the strong one.

    **Two sources are read**, and ``source`` says which one a row came
    from:

    * ``"assembly"`` — a table of address words, ``.word`` or ``.long``
      in a vector section, which is what a CMSIS startup file writes.
    * ``"c"`` — an array whose elements are addresses of functions, which
      is what a build that generates its table produces.  Zephyr writes
      its external interrupts this way, with ``gen_isr_tables.py``.  These
      rows also carry ``table_name``, the array the slot belongs to.
    * ``"build"`` — the registration the build itself recorded, for a slot
      the other two could not name.  A generator writes a resolved ADDRESS
      into every slot that is in use, so those slots have no name in the
      source at all — and they are the interrupts the firmware actually
      services.  Measured on an nRF54L application: 284 of 290 slots name
      the spurious stub, and the 6 without a name are IRQ 89, 198, 219,
      228, 269 and 270, which these rows fill in.

      Such a row can carry ``argument``, the symbol the build passes to
      the handler.  Read it as an argument and not as a second handler:
      behind the ``nrfx_isr`` shim it is the real worker
      (``nrfx_power_clock_irq_handler``), while for another driver it is
      the device (``__device_dts_ord_116``).  When the build enables
      run-time registration, a dict with ``info`` says so, because an
      interrupt connected at run time leaves nothing to read and the rows
      are then not all of them.

    Recognition is by shape, never by name, so any array of function
    addresses is reported and the row names its table.  A table of
    interrupt handlers and a table of state machine steps are the same
    construct, and ``table_name`` is how they are told apart.

    **Slot numbers are not joined across tables.**  Each slot is the index
    inside its own table, so two tables both start at 0 — read ``slot``
    together with ``table_name`` and ``source``.  They are not renumbered
    into one run because the index does not hold the length of the
    assembly table, only its occupied slots, and an offset derived from
    that would be silently wrong for every entry of a 290-entry table.

    **A ``coverage`` row follows the slots** for each table longer than the
    number of slots that name a function.  It says how many of the declared
    elements were named and which slot numbers were not, because a name is
    not always there to be read: an element can be a zero, or an address
    the linker resolved before the table was written.

    Read it in both directions.  A hole in a table of handlers is a vector
    nothing services.  A hole in Zephyr's ``_sw_isr_table`` is the
    opposite — measured on an nRF54L application, 284 of 290 slots name the
    spurious stub and the 6 without a name are the interrupts in use.  The
    tool reports where to look; which meaning applies depends on the table.

    **An ``interrupts`` row answers "which are unserviced"** wherever the
    build recorded its registrations, and it is the answer under
    ``unhandled_only`` too.  The row-level ``unhandled`` status is read
    from an alias edge, which a CMSIS startup writes and a generator does
    not — measured, zero unhandled rows on all eleven images of a Zephyr
    project against 39 to 72 on four CMSIS and Mbed ones.  The
    registrations settle it from the other side: what the build connected
    is the whole list, so anything else has nothing servicing it, and no
    handler has to be recognised by name.

    The complement is taken over the length of the table, NOT over the
    slots that hold a stub.  Measured on an mcuboot image: its software
    table names 44 of 48 slots, and one of those 44 is
    ``uarte_0_direct_isr``, an interrupt wired straight into the vector
    table.  It IS serviced, and counting stubs would report it as not.

    What is still not covered: an architecture that builds its table from
    branch instructions (arm64, Xtensa, MIPS) writes no table of
    addresses in either form.  A handler whose address the build resolved
    at link time has no name to report either — ``coverage`` names its slot
    but not the function.  For an interrupt this tool cannot show,
    ``find_references`` on the handler name still gives every reference
    the index holds.

    Read-only. No side effects. Requires an index of the assembly
    (``fw-context index``).

    Args:
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        unhandled_only: When True, return only the ``"unhandled"`` slots.
        limit: Maximum slots (default 400, max 1000).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts sorted by source, then table, then slot.  Each holds:
        slot (int), name, file, line, source (``"assembly"``, ``"c"`` or
        ``"build"``),
        status (``"c"``, ``"assembly"``, ``"unhandled"``, ``"runtime"``,
        ``"data"``, ``"linker"`` or ``"dispatcher"``), and table_file and table_line
        (where the slot is written).  A ``"c"`` source row also holds
        table_name and table_usr.  A ``"c"`` status row can hold
        overridden, a dict with file and line.  Any assembly row can hold
        installed, a list of dicts with name, file, line and at.

        Never empty: one dict with ``error`` (no index) or ``info`` (no
        vector table in this build).  Check both keys first.  A dict with
        ``coverage`` follows the slots for each table that has unnamed
        elements, and a dict with ``interrupts`` says which are connected
        and which are not — the latter in both modes.  Neither is subject
        to ``limit``: they describe the whole table, and the longest table
        is where they matter most.  When more slots exist than ``limit``,
        a dict with ``truncated`` sits between the slots and those two,
        saying how many slots are not shown.
    
- **find_callees_recursive** — Find all transitive C/C++ callees — what *name* calls, directly or
    indirectly, through the libclang call graph including
    function-pointer edges, implicit constructors, and synthetic
    dispatch edges. libclang-powered: follows function-pointer
    calls and indirect invocations across the full dependency tree.

    Use for dependency analysis: "what does this function depend on to do
    its job?"  Returns callees at depth 1 (direct), depth 2 (callees of
    callees), up to ``max_depth`` (default 5).  Results are deduplicated
    by shortest distance.

    **Edge types traversed:** Includes ``call``, ``indirect`` (function
    pointers / ISRs), ``implicit_construct`` (constructors reachable through
    file-scope global objects), and ``dispatch`` (synthetic edges through
    event loops and thread starts).

    **Limitation — ambiguous name resolution:** When a source-line fallback
    cannot disambiguate which method is called, the edge is conservatively
    omitted to avoid false callees.  If you suspect a missing callee,
    verify with ``search_bodies("target_name")``.

    For direct callees only, ``get_symbol_context`` gives a faster flat
    list along with the function body and callers. For the reverse
    direction use ``find_all_callers_recursive``.

    Read-only. No side effects. Requires the reference index
    (``fw-context index`` — refs on by default).

    Args:
        name: Symbol name to find transitive callees of.
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        max_depth: Maximum BFS depth for transitive search (default 5).
        limit: Maximum results (default 50).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts, each with: callee (str — callee name),
        callee_qualified_name (str), depth (int — distance from source),
        file (str), line (int), ref_kind (``"call"`` or ``"indirect"``).

        Never empty: one dict with ``error`` (cannot resolve) or ``info``
        (no results) replaces the results.  Check both keys first.
    
- **find_callers** — Find who calls a C/C++ function — direct calls AND indirect via
    function pointers, callbacks, interrupt vector registrations, and
    struct init lists. libclang-powered: detects function-pointer
    assignments and ISR vector registrations that text-based search
    cannot see.

    Falls back to macro lookup when the symbol is not found as a
    function/method: returns the macro definition (kind="macro") and
    files that use it (ref_kind="macro_use").

    Use when you need a quick, flat list of immediate callers. For the full
    transitive call tree (who calls this indirectly through other functions),
    use ``find_all_callers_recursive``.  For all references including reads
    and member accesses, use ``find_references``.  For a path between two
    specific symbols, use ``find_call_path``.

    Read-only. No side effects. Requires the reference index
    (``fw-context index`` — refs are on by default).  Only direct call
    sites are returned; callers more than one hop away are not included.

    Indirect edges (``ref_kind: "indirect"``) are detected when a function
    pointer references a function through:

    - **Call arguments**: ``callback(&Class::method, this)``,
      ``EventQueue::call_every(ms, obj, &handler)``
    - **Assignments**: ``driver.onData = &handleData``,
      ``global_cb = &handler``
    - **Variable initializers**: ``static void (*fp)(int) = &handler``
    - **Struct/array init lists**: ``{.on_data = &handler}``,
      ``{&fn_a, &fn_b}``

    Args:
        name: Symbol name to find callers of. Uses the same three-tier
            resolution as ``find_references`` (exact name, exact qualified,
            suffix LIKE).
        project_root: Project root directory. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        limit: Maximum results (default 50).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts, each with: file, line, ref_kind (``"call"``,
        ``"indirect"``, ``"implicit_construct"``, or ``"macro_use"``),
        caller (enclosing function name), caller_kind (``"function"``,
        ``"method"``, …). Macro fallback includes a leading dict with
        ``kind="macro"``, ``value``, and ``expanded_value``.

        Never empty: one dict with ``error`` (symbol not resolved) or
        ``info`` (no references of this kind).  Check both keys first.
    
- **find_dead_code** — Find C/C++ functions that are defined but never called —
    libclang-powered dead code detection across the entire indexed
    codebase. Distinguishes called from uncalled symbols globally, not
    just within a single file — text-based search cannot determine
    whether a function is actually reachable.

    **What "dead" means:** zero references in the index — no call, no
    function-pointer assignment, no indirect call site.  This is a
    single-layer reference check, NOT a reachability analysis from the
    entry points (main, ISR, exported symbols): a function that only a
    second dead function calls still has a reference, thus this tool does
    not mark it.  For transitive reachability, trace from your entry points
    with ``find_callees_recursive``.

    The ``status`` field splits the results:

    * ``"dead"`` — no reference at all.  Likely unused.
    * ``"possibly_dead"`` — assigned to a function pointer (Phase 1
      ``ref_kind="indirect"``), but no call site through that pointer
      resolved (Phase 3).  Unindexed code or a type-erased API can still
      call it.  Treat it as uncertain, and check each hit with
      ``find_indirect_targets`` before you delete anything.

    fw-context detects a constructor call through global/static object and
    member-field initialization as an ``implicit_construct`` reference.
    Known false positives remain: constructors from factories, ISRs,
    virtual method overrides, and weak-aliased symbols.  Always verify
    before you delete.

    ``project_only=True`` (default) excludes the SDK and vendor paths
    through the ``is_project`` column, which follows the ``vendor_paths``
    and ``project_paths`` config.  Set ``project_only=False`` to see the
    vendor results too.

    Read-only. No side effects. Requires the reference index
    (``fw-context index`` — refs on by default).

    Args:
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        limit: Maximum results (default 100).
        exclude_paths: Additional LIKE patterns to exclude (user-supplied
            tool parameter, not config). E.g. ``['lib/%']``.
        project_only: When True (default), filters to ``is_project = 1``
            symbols. Set False to see all results.
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts, each with: name, qualified_name, kind, file, line,
        status (``"dead"`` or ``"possibly_dead"``), and reason (str —
        explains why the function is classified as dead or possibly dead).

        Never empty: one dict with ``info`` replaces an empty result.
        Check that key first.
    
- **find_hotspots** — Find the most-called C/C++ functions ranked by caller count —
    libclang call-graph hotspot detection. Identifies functions with
    the most architectural weight — good targets for refactoring,
    optimization, or extra testing. Text-based search cannot aggregate
    caller statistics across the full call graph.

    Use for high-level impact assessment: changing a hotspot affects many
    call sites. The result tells you which functions carry the most
    "architectural weight" across the entire codebase.

    By default, SDK/vendor paths are auto-excluded so hotspots reflect
    project code. Use ``project_only=False`` to see all results including
    vendor code.

    For the callers of a specific hotspot, follow up with ``find_callers``
    or ``find_all_callers_recursive``.

    Read-only. No side effects. Requires the reference index
    (``fw-context index`` — refs on by default).

    Args:
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        limit: Number of top-called functions to return (default 20).
        project_only: When True (default), filters to ``is_project = 1``
            symbols so hotspots reflect project code.
        exclude_paths: Additional LIKE patterns to exclude (user-supplied
            tool parameter). E.g. ``['lib/%']``.
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts, each with: name, qualified_name, kind, file, line,
        caller_count (int — total number of call sites), signature.

        Never empty: one dict with ``info`` replaces an empty result.
        Check that key first.
    
- **find_indirect_call_sites** — Find indirect call sites where a C/C++ function pointer field or
    variable is invoked. libclang-powered: resolves calls through
    function pointers (e.g. ``driver.onData(buf, len)``), which
    text-based search cannot detect.

    Returns locations where a function pointer is called through a field
    access (``driver.onData(buf, len)``) or variable dereference
    (``stored_callback(42)``).

    Read-only. No side effects. Use this to answer "where is this function
    pointer invoked?" as opposed to ``find_callers`` which answers "who
    calls this function?" and ``find_references`` which answers "where is
    this symbol read or assigned?"

    For the reverse query — which functions are assigned to a given field
    or parameter — use ``find_indirect_targets``.

    Requires the reference index (``fw-context index`` — refs on by default).

    Args:
        name: Name of the function pointer field or variable.
            E.g. ``"onData"`` finds every call through a field named
            ``onData``.  Uses three-tier resolution: exact name, exact
            qualified, suffix LIKE.
        project_root: Project root directory. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        limit: Maximum results (default 50, max 200).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts, each with: file, line, expr_text (the callee
        expression, e.g. ``"driver.onData"``), target_usr, target_name,
        fn_ptr_type (the function pointer type signature), caller
        (enclosing function name), caller_kind.

        Never empty: one dict with ``error`` (cannot resolve) or ``info``
        (no results) replaces the results.  Check both keys first.
    
- **find_indirect_targets** — Find functions assigned to a C/C++ function pointer field or
    variable. libclang-powered: links assignment sites to call sites
    via the field's unique symbol reference, which text-based search
    cannot resolve.

    Links assignment sites (``driver.onData = &handler``) to call
    sites (``driver.onData(buf, len)``) via the field's USR.

    Returns each function that could be invoked through the named function
    pointer, showing both the assignment location and the call site(s).
    When a function is assigned but no call site is found, ``call_file``
    and ``call_line`` are ``null`` — the assignment exists but the
    invocation may be in unindexed code.

    For the reverse query — where is this field or parameter called — use
    ``find_indirect_call_sites``.

    Read-only. No side effects. Requires the reference index
    (``fw-context index`` — refs on by default).

    Args:
        name: Name of the function pointer field, variable, or parameter.
            E.g. ``"onData"`` finds every function assigned to a field
            named ``onData``.  Uses three-tier resolution.
        project_root: Project root directory. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        limit: Maximum results (default 50, max 200).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts, each with: rhs_name (assigned function),
        rhs_qname, fn_ptr_type, method (assignment/call_arg/var_init/
        init_list), assign_file, assign_line, assign_caller,
        call_file, call_line, call_expr_text.

        An entry can carry ``_note`` (str) when fw-context cannot resolve
        the direct call site — the callee is template-obscured, or the call
        site comes from the type-based fallback.  Read that note before you
        act on ``call_file`` and ``call_line``.

        Never empty: one dict with ``error`` (cannot resolve) or ``info``
        (no results) replaces the results.  Check both keys first.
    
- **find_references** — Find ALL references to a C/C++ symbol — calls, reads, member accesses,
    function pointer registrations, template references, and macro
    usages. libclang-powered: detects function-pointer registrations
    (interrupt vector table writes, callback attachments, ISR handler
    assignments) that text-based search cannot see.

    Falls back to macro lookup when the symbol is not found as a
    function/method: returns the macro definition (kind="macro") and
    files that reference it (ref_kind="macro_use").

    Read-only. No side effects. Returns every reference in the indexed codebase,
    including call sites, variable reads, struct member accesses, indirect
    function-pointer references, and macro usages. Requires the reference
    index (``fw-context index`` — refs on by default).

    For direct callers only use ``find_callers``. For transitive callers use
    ``find_all_callers_recursive``. For call paths between two symbols use
    ``find_call_path``.

    Args:
        name: Symbol name to find all references of.
        project_root: Project root directory. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        limit: Maximum results (default 50, max 200).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts, each with: file, line, ref_kind, caller, caller_kind.
        ``ref_kind`` is one of: ``"call"``, ``"ref"``, ``"member"``,
        ``"indirect"`` (function-pointer reference in arguments, assignments,
        initializers, or init lists), ``"implicit_construct"`` (implicit
        constructor call from global/static object or member-field
        initialization), ``"macro_use"`` (macro usage
        in file). Macro fallback includes a leading dict with
        ``kind="macro"``, ``value``, and ``expanded_value``.

        Never empty: one dict with ``error`` (symbol not resolved) or
        ``info`` (no references).  Check both keys first.
    
- **find_wrapper_callers** — Find C/C++ wrapper classes that call methods of a driver class —
    libclang-powered adapter pattern detection. Traces method ownership
    across class boundaries to reveal the wrapper/adapter architecture
    (e.g. ``UART`` wraps ``UART_DRIVER``). Text-based search cannot
    distinguish which class owns each method call.

    Returns wrapper methods grouped by wrapper class, showing which driver
    methods each wrapper calls.  Useful for understanding the adapter/wrapper
    architecture (e.g. ``UART`` wraps ``UART_DRIVER``).

    For the reverse perspective — finding who calls a specific driver method
    — use ``find_callers``. For class member listing use
    ``get_class_members``.

    Read-only. No side effects. Requires the reference index
    (``fw-context index`` — refs on by default).

    Args:
        class_name: Driver class name to find wrappers for.
            E.g. ``'UART_DRIVER'`` or ``'hal::UART_DRIVER'``.
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        limit: Maximum wrapper method results (default 50).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts, each with: wrapper_class (str — ``"(global)"`` for a
        free function), method_count (int),
        methods (list of dicts — each with method, qualified_name, kind,
        file (str — absolute path of the file that holds the body of that
        method), and calls (list of dicts — ``driver_method`` (str) and
        ``line`` (int) of each call into the driver))).

        The path sits on the method, not on the class, because one wrapper
        class often spans several files.

        Never empty: one dict with ``error`` (cannot resolve) or ``info``
        (no results) replaces the results.  Check both keys first.
    
- **trace_data_flow** — Trace how C/C++ data of a given type flows to a target function via
    libclang call paths. libclang-powered: finds functions by type
    signature and maps call paths through the full call graph, which
    text-based search cannot trace across translation units.

    Finds functions whose signature mentions *type_name*, then looks for call
    paths from those functions to *to_symbol*.  Returns a data flow map —
    useful for understanding how a data structure travels through the system
    to its destination.

    Works best for synchronous driver stacks (e.g. sensor read → I2C write).
    Cannot follow async flows (message queues, interrupts, RS485 callbacks).
    For exact call-graph queries use the ``find_*`` family;
    verify specific paths with ``find_call_path``.

    Read-only. No side effects. Requires the reference index
    (``fw-context index`` — refs on by default).

    Args:
        type_name: Type name to trace. E.g. ``'SensorData'`` or
            ``'Config::SensorData'``.
        to_symbol: Target symbol name. E.g. ``'uart_send'`` or
            ``'UART_DRIVER::send'``.
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        max_depth: Maximum call path depth (default 8).
        limit: Maximum source functions to trace (default 15).
        timeout_ms: Maximum total execution time in milliseconds
            (default 30000). Clamped to 1000–300000.
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts with a leading ``_summary`` entry:
        {_summary (str), _type (str), _target (str)}, followed by source
        entries each with: source_name, source_qualified_name, source_kind,
        source_file, source_line, caller_count, reachable (bool), and
        paths (list of call path dicts — empty when unreachable).

        A source entry with ``timed_out: True`` means that the path search
        stopped at the time limit for that source.  Its ``reachable: False``
        thus means "not proved reachable", not "proved unreachable".

        Never empty: one dict with ``info`` replaces an empty result.
        Check that key first.
    
- **explain_symbol** — Explain what a C/C++ symbol does in plain English — libclang-aware
    analysis. Uses pre-computed LLM analysis when available (instant),
    falls back to on-demand LLM. Falls back to macro explanation when
    the name matches a ``#define``.

    Read-only. No side effects — uses pre-computed LLM analysis when available
    (instant, generated during ``fw-context index --analyze``), falls back to
    calling an LLM on-demand. Returns the symbol's purpose, inputs, outputs,
    and side effects.

    For raw source code use ``get_source``. For symbol metadata without
    explanation use ``lookup_symbol``. For body + callers + callees use
    ``get_symbol_context``.

    Args:
        name: Symbol name to explain. E.g. ``uart_init``, ``ModemMsg::send``.
        project_root: Project root directory. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        context_lines: Lines of source context around the symbol definition
            (default 40, max 200). Only used when no pre-computed analysis exists.
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        dict: {name, kind, file, line, signature, explanation, llm_analysis
        (if pre-computed)}, plus source/explain_prompt on fallback. Macro
        fallback returns ``kind="macro"``, ``signature`` (as ``#define NAME``),
        ``value`` (raw definition), and ``expanded_value``.

        A ``warning`` key means that the local LLM gave no explanation —
        the request timed out, or the model is not available.  The dict then
        holds ``source`` and ``explain_prompt``: read the source, and answer
        the prompt yourself.

        When the file changed after the last index run, the dict adds
        ``stale`` (True) and ``stale_warning`` (str).  ``stale_warning`` is
        separate from ``warning``, which the LLM error paths use.  A symbol
        that moved gives its indexed body, not the code that now sits at the
        stored line number.

        On failure the dict holds only ``error`` with the reason.
    
- **get_file_map** — Fast structural map of all C/C++ symbols in a file grouped by kind —
    libclang-powered table of contents. Like a table of contents before
    reading a chapter: see what functions, classes, and enums a file
    defines at a glance.

    Paths are validated against the project root before read —
    :func:`_validate_path_in_root` ensures resolved paths stay within bounds.

    Pass a path relative to the project root (``src/main.cpp``) or just the
    filename (``main.cpp``). Returns symbols keyed by kind (function, method,
    class, struct, enum, ...). Each kind has count (total) and items (first N,
    default 30). Set max_per_kind=0 for unlimited, signatures=true for full sigs.

    Enum constants (``enum_constant``) are grouped into ``subgroups`` by
    parent enum. Each subgroup has ``name``, ``count``, and ``constants``
    (list of ``{name, qualified_name, line, enum_value}``). The subgroup
    count reflects the real total even when ``max_per_kind`` limits the
    constants list.

    For detailed symbol information use ``get_symbol_context`` or
    ``lookup_symbol``.

    Read-only. No side effects. Use before reading a large file to
    orient yourself — see what functions, classes, and enums it defines.

    Args:
        file_path: Path relative to project root, or just the filename.
        project_root: Project directory. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        signatures: Include full function signatures. Default: False.
        max_per_kind: Max items per kind group (default 30, 0 = unlimited).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        dict: {file, total_symbols, symbols: {kind: {count, items[],
        subgroups?[]}}}

        Each item holds ``name``, ``qualified_name``, and ``line``, plus
        ``end_line`` when the symbol is a definition.  The two line numbers
        are the extent, thus ``file:line-end_line`` is the citation.

        On failure the dict holds only ``error`` with the reason.
    
- **get_source** — Read a C/C++ function/method/enum/macro body using libclang exact
    extents — no guessing line numbers. Uses AST-precise {start, end}
    extents so you get exactly the function body. Generic file readers
    don't know where a function actually ends — libclang tracks exact
    {start, end} from the AST.

    For enums, includes a ``constants`` array listing all member constants
    with their values. For macros, returns kind="macro" with ``value``
    (raw definition) and ``expanded_value`` (preprocessor-resolved).

    For rich context (who calls this, what does it call) use
    ``get_symbol_context`` instead — it returns body, callers, and callees
    in a single call. For the full file, use a normal file read.

    Read-only. No side effects.

    Args:
        name: Fully qualified symbol name. Returns exact function body
            via libclang extent.
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        dict: {name, qualified_name, kind, file, line, signature,
        docstring, is_definition, is_template, is_virtual, is_pure_virtual,
        source (str — the function/enum/macro body, truncated at 8000 chars),
        warning (str, optional — when source file cannot be read)}.
        May also include ``end_line`` (the last line of the extent),
        ``template_usr``, ``parent_usr``, ``enum_value``, ``constants``
        (list for enums), ``value`` (raw macro definition),
        ``expanded_value`` (preprocessor-resolved macro value) when
        applicable.  A declaration has no extent, thus it gets no
        ``end_line``.

        ``line`` and ``end_line`` are the extent of the symbol, thus they
        are the citation: quote ``file:line-end_line``.  Do not count the
        lines of ``source`` to find the end.

        ``source`` carries a line-number prefix on every line — four
        columns, right-aligned, then two spaces (``"  20     bool ..."``).
        This tool is the only one that numbers its text: the ``source`` of
        ``search_bodies`` and the ``content`` of ``read_file`` are both
        bare.  Strip the prefix before you compare the text with anything.

        When the file changed after the last index run, the dict adds
        ``stale`` (True) and ``stale_warning`` (str).  ``source_origin`` then
        tells where the body comes from: ``"disk"`` when the symbol did not
        move, ``"index"`` when it did and the body comes from the index
        instead.  A moved symbol never gives the code of another symbol.

        On failure the dict holds only ``error`` with the reason.
    
- **get_symbol_context** — Rich one-shot context for a C/C++ symbol: body, signature, all direct
    callers and callees. Answers "what does this do and how does it fit in
    the system?" in a single response — libclang powers the call graph,
    not regex. Falls back to macro display when the symbol is not found.

    Prefer this over ``get_source`` when you also need callers, callees,
    indirect call sites, or LLM analysis — all returned in a single call.
    If you only need the raw function body (no metadata), ``get_source`` is
    slightly faster. For transitive call-graph exploration use
    ``find_all_callers_recursive`` or ``find_callees_recursive``.

    Returns ALL callers and callees including vendor/SDK code — the call
    graph naturally spans project and vendor boundaries in both directions
    (project → vendor API, vendor callback → project handler).

    Read-only. No side effects.

    Args:
        name: Symbol name. Returns body, signature, all direct callers
            and callees.
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        dict with: name, qualified_name, kind, file, line, signature,
        docstring (raw Doxygen comment text), is_definition, callers (list),
        callees (list), source (body text),
        indirect_call_sites (list, for field/variable symbols — where the
        function pointer is actually invoked).
        For field and variable symbols that have function pointer type,
        also includes ``resolution``: {assignments_found, call_sites_found,
        resolved, note} indicating whether assignments and call sites are
        linked (Phase 3).  ``resolved=False`` with a note when parts are
        missing — LLM can detect uncertainty.
        For enums also returns constants and enum_value.
        For macros returns ``kind="macro"``, ``value`` (raw definition), and
        ``expanded_value`` (preprocessor-resolved).
        When LLM analysis has been generated (``fw-context index --analyze``),
        includes ``llm_analysis``: {summary, inputs, outputs, model, analyzed_at}
        with a structured description of the symbol's purpose, parameters, and
        return values/side effects.

        When the file changed after the last index run, the dict adds
        ``stale`` (True) and ``stale_warning`` (str), and ``source_origin``
        tells where the body comes from: ``"disk"`` when the symbol did not
        move, ``"index"`` when it did.  The callers and callees come from the
        index in all cases, thus a stale dict can hold an incomplete list.

        The dict also carries the libclang flags of the symbol:
        is_virtual, is_pure_virtual, is_template, parent_usr, and
        template_usr.  For a virtual method it adds ``overrides`` (the base
        methods that this method overrides) and ``overridden_by`` (the
        derived methods that override it) — use these two before you change
        a virtual method.

        On failure the dict holds only ``error`` with the reason.
    
- **read_file** — Read a complete C/C++ source file with **ifdef-filtered** content —
    only code that actually compiles for the current build configuration.
    Inactive ``#ifdef`` branches are replaced with blank lines (preserving
    original line numbers).

    Use this to read a file without leaving the fw-context ecosystem.
    Unlike generic file readers, this tool returns build-accurate content:
    code gated behind ``#ifdef BOARD_V2`` stays visible only when
    ``BOARD_V2`` is actually defined for this build.  Line numbers match
    the original file — inactive branches appear as blank lines, and the
    text spans the whole file, thus ``lines`` is the length of the file.

    ``content`` is bare text by default and carries NO line-number prefix —
    unlike the ``source`` of ``get_source``, which numbers every line.
    Never count the lines here to find a number.  Take it from a field
    instead: the ``match_lines`` of ``search_bodies`` or ``search_content``,
    the ``line`` / ``end_line`` of ``get_source`` and ``get_file_map``, or
    pass ``line_numbers=True`` and read the number off the line.

    ``start_line`` and ``end_line`` cut a window out of the file (1-based,
    both ends inclusive, 0 = no bound on that side).  Reading around a
    known line costs a fraction of the whole file — 40 lines around a match
    instead of 2000 lines of a header.

    An include guard is a blank line: ``#ifndef`` and ``#endif`` are
    conditional directives, which carry no token and thus never count as
    active.  The line stays in place, and only its text is gone.

    For reading a single function body with libclang exact extents use
    ``get_source``.  For body + callers + callees in one call use
    ``get_symbol_context``.  For a structural overview without content use
    ``get_file_map``.  For searching patterns across files use
    ``search_content``.

    Read-only. No side effects.  Falls back to raw disk content (with a
    warning) when the indexed ``files.content`` column is empty — e.g. on
    a legacy index that predates this feature.  Run ``fw-context index``
    to populate the ifdef-filtered content.

    Args:
        file_path: Path relative to project root, or just the filename.
            E.g. ``src/main.cpp`` or ``main.cpp``.
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        line_numbers: Prefix every line with its number, right-aligned and
            followed by two spaces, as ``get_source`` does. Default False.
        start_line: First line to return, 1-based inclusive. 0 = file start.
        end_line: Last line to return, 1-based inclusive. 0 = file end.
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        dict: {file (str), language (str — ``"c"`` or ``"cpp"``),
        mtime (float), lines (int — total line count of the WHOLE file,
        whatever range was asked for),
        content (str — the ifdef-filtered text, bare unless
        ``line_numbers`` was set),
        warning (str, optional — when reading from raw disk instead of
        indexed content)}.

        A range adds ``start_line`` and ``end_line`` — the first and last
        line the ``content`` really holds, after the end was clamped to the
        length of the file.

        On failure the dict holds only ``error`` with the reason: a
        negative bound, an ``end_line`` before ``start_line``, or a
        ``start_line`` past the end of the file.
    
- **get_class_members** — Return all methods, fields, and nested types of a C/C++ class/struct —
    libclang-powered member table. Groups members by kind (method,
    constructor, field, enum, etc.), distinguishing class members from
    free functions across the entire codebase.

    Members are grouped by kind (method, constructor, destructor, field, enum,
    typedef, class, struct). Each member includes its signature, virtual flags,
    and source line. Works for C structs too — they just won't have methods.

    For inheritance hierarchy use ``get_inheritance_chain``. For individual
    method details use ``get_symbol_context``.

    Read-only. No side effects.

    Args:
        class_name: Class or struct name. E.g. ``'ModemManager'`` or
            ``'comm::MODEM'``.
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        dict: {name, qualified_name, kind, file, line, members: {kind:
        [{name, qualified_name, signature, is_virtual, is_pure_virtual,
        line}]}, member_count}

        On failure the dict holds only ``error`` with the reason.
    
- **get_inheritance_chain** — Return the C++ inheritance chain for a class or struct —
    libclang-aware hierarchy. Resolves base/derived class relationships
    across all translation units, which single-file reading cannot do.

    Shows direct base classes (what this inherits from) and direct derived
    classes (what inherits from this), along with access level and virtual
    flag for each edge.

    When ``transitive=True``, walks the full hierarchy up to all ancestors
    and down to all descendants (bounded by ``max_depth``). Uses BFS with
    cycle detection to handle diamond inheritance.

    For class members use ``get_class_members``. For virtual method
    override chains use ``get_method_overrides``.

    Read-only. No side effects.

    Args:
        class_name: Class or struct name to get inheritance information for.
            E.g. ``'UART_DRIVER'`` or ``'comm::MODEM'``.
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        transitive: When True, walk the full inheritance tree both up
            (ancestors) and down (descendants). Default: False (direct
            bases and derived only).
        max_depth: Maximum BFS depth for transitive walk (default 10,
            clamped to 1–50).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        dict: {
            name, qualified_name, kind, file, line,
            bases: [{name, usr, access, is_virtual, file}],
            derived: [{name, usr, access, is_virtual, file}],
            all_bases: [...] (when transitive=True, ancestors sorted by depth),
            all_derived: [...] (when transitive=True, descendants sorted by depth)
        }

        On failure the dict holds only ``error`` with the reason.
    
- **get_method_overrides** — Return C++ virtual method override information — libclang-powered
    vtable analysis. Resolves virtual dispatch across class hierarchies:
    shows which base-class method this overrides, and which derived-class
    methods override this one. Text-based search cannot resolve virtual
    dispatch across translation units.

    Shows what base-class method this method overrides, and what derived-class
    methods override this one.  Built from the ``overrides`` table which is
    populated during ``fw-context index`` via post-processing of the inheritance
    graph and virtual method signatures.

    For class-level inheritance, use ``get_inheritance_chain``.  For symbol
    details, use ``get_symbol_context``.

    Read-only. No side effects.

    Args:
        method_name: Method name to get override information for. Use
            qualified name for disambiguation, e.g.
            ``'UART_DRIVER::write'``.
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        dict: {
            name, qualified_name, kind, file, line, signature,
            overrides: [{usr, name, qualified_name, kind, file, line}],
            overridden_by: [{usr, name, qualified_name, kind, file, line}]
        }

        On failure the dict holds only ``error`` with the reason.
    
- **get_template_instances** — Find all template instantiations for a C/C++ class or function
    template — libclang template-aware lookup. Finds concrete
    instantiations spread across all translation units, each with its
    full type signature. Text-based search cannot resolve template
    specializations across translation units.

    Returns concrete instantiations of the template — each with its full type
    signature (e.g. ``Callback<void(int)>``).  The template declaration itself
    is also returned as the first result when found.

    Uses the ``template_usr`` column populated during indexing via libclang's
    ``cursor.specialized_template``.

    **Known limitation:** libclang's ``specialized_template`` does not
    reliably resolve implicit instantiations or template methods of
    template classes.  Header-only templates (e.g. ``RingBuffer<T>``)
    may report zero instances even when used in the codebase.  Explicit
    specializations and class/struct instantiations are detected more
    reliably than method-level instantiations.

    For finding the template declaration itself use ``lookup_symbol``.

    Read-only. No side effects.

    Args:
        template_name: Template name to find instantiations for.
            E.g. ``'Callback'`` or ``'mbed::Callback'``.
        project_root: Project root. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        limit: Maximum results (default 50).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list[dict] with one element wrapping the template declaration:
        {name, qualified_name, kind, file, line, is_definition,
        signature, instances (list of dicts, each with name,
        qualified_name, kind, file, line, signature, is_definition),
        instance_count (int)}

        No match gives ``[]``.  One dict with ``error`` means the query
        failed — check that key first.
    
- **find_variables** — Find C/C++ variables by name or prefix and trace who reads or
    writes them through the call graph.  libclang-powered: splits
    variables into global (``varglobal`` — file/namespace/class-scope)
    and local (``varlocal`` — inside a function body).

    Each result includes a type signature (``bool timeSet``,
    ``const IPAddress modbus_ip``), the enclosing function for locals
    (``"<file scope>"`` for globals), and a ``references`` list showing
    every function that reads or writes the variable — the same
    ``ref_kind`` values as ``find_references`` (``"call"``, ``"ref"``,
    ``"member"``).

    Use when you need to understand shared state, find who modifies a
    global variable, trace side effects, or distinguish important globals
    from loop counters.  For general symbol search use ``search_code`` or
    ``lookup_symbol``.  For all references to a specific variable
    (including reads in expressions), use ``find_references``.

    Legacy indexes with ``kind="variable"`` (pre-split) are detected and
    included in results — reindex to fully benefit from the split.

    Read-only. No side effects.

    Args:
        name: Variable name or prefix to search. Uses LIKE match
            (e.g. ``g_`` finds ``g_debug_level``, ``g_state``).
        project_root: Project root directory. Auto-detected if omitted.
        project: Project name or project_id — call list_projects to get them.
            Use it to ask about a project that is not the project of the
            current directory. It is an alternative to project_root, which
            takes a root path. Give one of the two, not both.
        kind: Optional kind filter — ``"varglobal"``, ``"varlocal"``,
            ``"field"``, or ``None`` (all). Default ``None``.
        limit: Maximum results (default 20, max 100).
        variant: Build variant (multi-project). Omit for the default
            variant, ``"*"`` for all.
        image: Sysbuild image in the variant. Omit for all images.

    Returns:
        list of dicts, each with: name (str), qualified_name (str),
        kind (str — ``"varglobal"`` or ``"varlocal"``), file (str),
        line (int), signature (str — e.g. ``"const IPAddress modbus_ip"``),
        enclosing_function (str — function name for varlocal,
        ``"<file scope>"`` for varglobal), enclosing_class (str — class
        name for static members, empty otherwise),
        references (list[dict] — ``function``, ``file``, ``line``,
        ``ref_kind``).

        No match gives ``[]``.  One dict with ``error`` means the query
        failed — check that key first.
        A ``warning`` key marks a partial result.
    

## Claude Desktop Quick Installation
Install path detected from listing signals. Uses `uvx` (confidence: high):

```json
"mcpServers": {
  "fw-context-mcp": {
    "command": "uvx",
    "args": ["fw-context-mcp"]
  }
}
```

## Documentation

## What turbyho/fw-context-mcp does

The turbyho/fw-context-mcp MCP server gives coding agents a build-aware view of embedded C and C++ firmware. It reads the project's compilation database and parses translation units with libclang, allowing queries to follow the active compiler flags, preprocessor configuration, selected sources, and vendor/project boundaries.

The resulting persistent index supports symbol metadata, definitions, signatures, documentation, macros, filtered file content, references, direct and indirect calls, inheritance relationships, and build information. It can also record optional LLM-generated summaries and embeddings. The data is stored locally in SQLite, with FTS5 indexes for lexical searches.

Reach for turbyho/fw-context-mcp when repository-wide text search is insufficient—for example, when an agent must identify the implementation selected by a target build, trace callbacks or interrupt registrations, inspect callers across translation units, or compare project code with SDK code.

## How it works

A project first needs a usable compile_commands.json. Indexing passes those commands and source files through libclang, then records symbols and relationships in a reusable database. The server can track multiple projects, build variants, and sysbuild images where the configuration exposes them.

Search tools cover different retrieval needs:

- `lookup_symbol` resolves exact names or prefixes.
- `search_code` searches symbol names, signatures, documentation, and tokenized names.
- `search_bodies` searches the stored text inside definitions.
- `search_content` searches build-filtered full-file content, including preprocessor directives and file-scope text.
- `semantic_search` uses precomputed embeddings when an embedding model is available.
- `smart_search` uses an LLM to generate and refine search terms before querying the index.

Call-graph tools inspect direct callers and callees, recursive paths, function-pointer assignments, indirect call sites, references, wrappers, hotspots, and possible dead code. Firmware-specific queries include build status, linker memory information, vector tables, variants, and data-flow paths. `get_active_build` is the required first health check for C/C++ projects before other project queries.

## Setup and configuration

Install the Python package with `pip install fw-context-mcp`, then initialize the project from its firmware directory with `fw-context init`. Create or obtain a compilation database, and build the initial index with `fw-context index --build`. Later runs can update changed translation units incrementally; individual files can also be refreshed through the reindex tools when they appear in compile_commands.json.

The documented prerequisites are Python 3.11 or newer, libclang, and a project that can produce compile_commands.json. The project supports build setups including Zephyr, PlatformIO, Mbed OS, Arduino, ESP-IDF, generic CMake, Makefile-based projects, and custom builds that provide a compilation database.

LLM settings are optional. `configure_llm` writes project-local settings to `.fw-context/local.toml` rather than changing the shared project configuration. It can target Ollama or an OpenAI-compatible endpoint. If an external endpoint is configured, source snippets may be sent there, so deployment should follow the organization's data-handling rules.

## Tools and capabilities

The MCP tool surface includes read-only diagnostics for dependencies, project discovery, active-build health, variants, Ollama availability, and environment status. Index maintenance tools can reparse files or reset an index; reset requires explicit confirmation and permanently deletes the project's SQLite database and WAL files.

Reference and analysis tools can answer questions such as:

- Which functions call or reach a target, including indirect edges?
- What does a function call transitively?
- Which functions are assigned to a callback field, and where is it invoked?
- Which symbols have no indexed references?
- What wrappers call methods on a driver class?
- Which interrupt slots are handled, unhandled, runtime-installed, or linker-defined?
- How can data of a specified type reach a target function?

## Limitations and notes

Results depend on the completeness and correctness of compile_commands.json. A source file absent from that database cannot be fully indexed until the build regenerates it. Header reindexing through one including translation unit covers only that compilation context; a full index is needed to cover every set of preprocessor definitions that includes the header.

Call-graph resolution is conservative when overloaded or template-obscured calls cannot be disambiguated, so some edges may be omitted. Dead-code results are reference checks, not full reachability proofs, and possible indirect references require verification. Data-flow tracing does not follow asynchronous queues, interrupts, or RS485 callbacks.

Semantic search and on-demand explanations require an available LLM or embedding backend. Without one, semantic search and smart search use fallback behavior, while compiler-derived symbol and reference queries remain available. Vector-table support also depends on the build producing recognizable address tables; architectures that construct vectors from branch instructions may not be covered.

## Getting started with this turbyho/fw-context-mcp MCP server
Always refer to the official documentation for the most accurate and up-to-date information.

_Full upstream README: https://allmcps.com/mcp/turbyho-fw-context-mcp/readme_

