Skip to content

feat: MCP server for documentation retrieval - #73

Open
Benkapner wants to merge 1 commit into
mainfrom
feat/mcp-server
Open

feat: MCP server for documentation retrieval#73
Benkapner wants to merge 1 commit into
mainfrom
feat/mcp-server

Conversation

@Benkapner

Copy link
Copy Markdown
Collaborator

Summary

Exposes the folder-index retrieval layer as a read-only MCP server. Any MCP-compatible agent can query which docs cover a given source file.

  • 3 tools: find_docs_for_code, get_doc_index, check_doc_drift
  • Read-only by design: no write tools (a server that edits docs is a separate security conversation)
  • docs/mcp.md: setup for Claude Code and other MCP clients
  • Uses the existing mcp package (already in dependencies)

Test plan

  • uv run pytest -v passes (417 tests)
  • Lint clean
  • uv run python src/mcp_server.py starts and responds to MCP protocol

The reusable asset in this repo is the folder-index retrieval layer.
"Which docs cover this code?" is a query any agent wants. Expose it
as a read-only MCP server with three tools: find_docs_for_code,
get_doc_index, and check_doc_drift. Read-only by design; no write
tools in this commit. Includes docs/mcp.md with setup instructions
for Claude Code and other MCP clients.
@Benkapner Benkapner self-assigned this Aug 17, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 17, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 5:56 AM UTC · Completed 6:11 AM UTC

Commit: 8cdd2d5 · View workflow run →

@Benkapner
Benkapner requested a review from csoceanu August 17, 2026 05:56
@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

Critical

  • [api-contract] src/mcp_server.py:91 — Both _find_docs_for_code and _get_doc_index read index content via info.get("index", "") from the manifest. The manifest (written by build_all_indexes and update_indexes_if_needed in doc_index.py) never stores an "index" key — it stores {"built": ..., "doc_hashes": ...} per folder. Actual index text lives in separate .index.md files and must be loaded via load_index() or load_all_indexes(). As a result, two of the three MCP tools will always return empty/not-found responses.
    Remediation: Replace manifest-based lookup with load_all_indexes(docs_root) in _find_docs_for_code and load_index(folder, docs_root) in _get_doc_index. Import these from doc_index.

High

  • [path-traversal] src/mcp_server.py:122_check_doc_drift constructs full_path = docs_root / doc_path from user-supplied input without validating the resolved path stays within the docs root. A path like ../../etc/passwd leaks file metadata (existence, line count, code-block presence) of arbitrary files on the filesystem. The codebase already provides validate_file_path() in security_utils.py for this purpose.
    Remediation: Resolve and validate the path before proceeding: full_path = (docs_root / doc_path).resolve(); if not full_path.is_relative_to(docs_root): return [TextContent(type="text", text=f"Invalid path: {doc_path}")].

Medium

  • [data-exposure] src/mcp_server.py:143 — Exception handler exposes raw exception message in MCP response (f"Error reading {doc_path}: {e}"). Python file-operation exceptions can include full filesystem paths. The codebase consistently uses sanitize_output() from security_utils for error output.
    Remediation: Import and apply sanitize_output: f"Error reading {doc_path}: {sanitize_output(str(e))}".

  • [missing-test] src/mcp_server.py — No test file exists for the new module. Project convention (CLAUDE.md) requires tests mirror source modules as test_<module>.py. All other src/ modules have corresponding test files.

  • [scope-exceeded] src/mcp_server.py — Introduces a standalone MCP server entry point not reflected in CLAUDE.md's documented architecture (entrypoint.sh → src/suggest_docs.py).
    Remediation: Update CLAUDE.md to document the MCP server as an additional entry point.

  • [missing-doc] README.md — The MCP server feature is not mentioned in README.md. The PR includes docs/mcp.md but the primary documentation entry point provides no path to discover it.

  • [pattern-inconsistency] src/mcp_server.py:17sys.path.insert(0, ...) is not used in any other module in the codebase. As a standalone entry point, consider using uv run python -m invocation or configuring a package entry point instead.

  • [stale-doc] CLAUDE.md:12 — Source modules table does not include mcp_server.py.
    Remediation: Add row: | mcp_server.py | MCP server — find_docs_for_code, get_doc_index, check_doc_drift tools |.

Low

  • [missing-authorization] No linked issue for this non-trivial feature addition (207 lines, new runtime mode).

  • [architectural-conflict] src/mcp_server.py — Module is standalone and not integrated with the main entry point, unlike all other src/ modules. This is inherent to MCP server design but should be documented.

  • [fail-open] src/mcp_server.py:81call_tool dispatcher passes arguments to handlers without input validation. Low risk given read-only design and MCP protocol-level schema validation.


Labels: PR adds a new MCP server feature in Python


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

Comment thread src/mcp_server.py
results = []

for folder, info in manifest.get("folders", {}).items():
index_text = info.get("index", "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[critical] api-contract

Both _find_docs_for_code and _get_doc_index read index content via info.get("index", "") from the manifest. The manifest never stores an "index" key — it stores {"built": ..., "doc_hashes": ...} per folder. Actual index text lives in separate .index.md files loaded via load_index() or load_all_indexes(). Two of three MCP tools always return empty/not-found responses.

Suggested fix: Replace manifest-based lookup with load_all_indexes(docs_root) in _find_docs_for_code and load_index(folder, docs_root) in _get_doc_index. Import these from doc_index.

Comment thread src/mcp_server.py
async def _check_doc_drift(doc_path):
"""Assess staleness of a single doc file."""
docs_root = get_docs_root().resolve()
full_path = docs_root / doc_path

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[high] path-traversal

_check_doc_drift constructs full_path = docs_root / doc_path from user-supplied input without validating the resolved path stays within docs root. A path like ../../etc/passwd leaks file metadata (existence, line count, code-block presence) of arbitrary files. The codebase provides validate_file_path() in security_utils.py for this purpose.

Suggested fix: Resolve and validate: full_path = (docs_root / doc_path).resolve(); if not full_path.is_relative_to(docs_root): return error. Alternatively, import and use validate_file_path from security_utils.

Comment thread src/mcp_server.py
),
)
]
except Exception as e:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] data-exposure

Exception handler exposes raw exception message in MCP response. Python file-operation exceptions can include full filesystem paths. The codebase consistently uses sanitize_output() from security_utils for error output.

Suggested fix: Import sanitize_output from security_utils and wrap: f"Error reading {doc_path}: {sanitize_output(str(e))}"

Comment thread src/mcp_server.py
from mcp.server.stdio import stdio_server
from mcp.types import TextContent, Tool

sys.path.insert(0, str(Path(__file__).resolve().parent))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] pattern-inconsistency

sys.path.insert(0, ...) is not used in any other module in the codebase. As a standalone entry point, consider using uv run python -m invocation or configuring a package entry point instead.

Comment thread src/mcp_server.py
return await _get_doc_index(arguments.get("folder", ""))
elif name == "check_doc_drift":
return await _check_doc_drift(arguments.get("doc_path", ""))
return [TextContent(type="text", text=f"Unknown tool: {name}")]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] fail-open

call_tool dispatcher passes arguments to handlers without input validation. Low risk given read-only design and MCP protocol-level schema validation.

@fullsend-ai-review fullsend-ai-review Bot added feature python Pull requests that update python code labels Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature python Pull requests that update python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant