feat: MCP indexing server and tree enrichment - #20
Conversation
Expose index_pdf over MCP and split CLI/MCP entrypoints so agents can index PDFs without the Typer CLI. Co-authored-by: Cursor <cursoragent@cursor.com>
After outline assembly, enrich each section and the whole document with LLM summaries so the tree is usable for retrieval without re-reading the PDF. Co-authored-by: Cursor <cursoragent@cursor.com>
Changed Files
|
Reviewer's GuideAdds an MCP indexing server using FastMCP and enriches the PDF indexing pipeline so assembled Trees carry node-level summaries and a structured document-level description, while keeping tree assembly structural-only for testability and updating project scripts and metadata accordingly. Sequence diagram for MCP index_pdf tool and enriched indexing pipelinesequenceDiagram
actor Agent
participant FastMCP_server as FastMCP_server
participant index_pdf
participant index as index
participant assemble_tree as assemble_tree
participant generate_node_summaries as generate_node_summaries
participant generate_doc_description as generate_doc_description
Agent->>FastMCP_server: MCP request index_pdf(path)
FastMCP_server->>index_pdf: invoke tool index_pdf(path)
index_pdf->>index: index(path)
index->>assemble_tree: assemble_tree(outline, document)
assemble_tree-->>index: Tree
index->>generate_node_summaries: generate_node_summaries(tree.nodes, document)
generate_node_summaries-->>index: [nodes enriched]
index->>generate_doc_description: generate_doc_description(tree.nodes)
generate_doc_description-->>index: DocumentDescription
index-->>index_pdf: Tree
index_pdf-->>Agent: str(Tree)
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
WalkthroughPDF indexing now creates node summaries and a document description. A new MCP server exposes PDF indexing. Tree models, prompts, command entrypoints, dependency metadata, configuration, and round-trip tests were updated. ChangesPDF indexing and MCP integration
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant MCPClient
participant index_pdf
participant PDFIndexer
participant CompletionClient
MCPClient->>index_pdf: index_pdf(path)
index_pdf->>PDFIndexer: index(path)
PDFIndexer->>CompletionClient: generate node summaries
CompletionClient-->>PDFIndexer: return summaries
PDFIndexer->>CompletionClient: generate document description
CompletionClient-->>PDFIndexer: return description
PDFIndexer-->>index_pdf: return completed tree
index_pdf-->>MCPClient: return tree string
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- The
generate_node_summariesandgenerate_doc_descriptionhelpers repeatedly callasyncio.runand create a new completion client per node/description; consider making the indexing flow async or batching calls to reuse a single client and event loop for better performance. - In
generate_doc_description, you currently passstr(nodes)to the model, which will include full Pydantic representations; it may be more efficient and predictable to serialize only the fields the model needs (e.g., titles, spans, and summaries) before sending to the LLM. - The page slicing logic in
generate_node_summaries(doc.pages[node.start_index - 1 : node.end_index]) relies on 1-based indices with an inclusive end; consider encapsulating this in a dedicated helper or adding explicit comments to avoid off-by-one mistakes in future changes.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `generate_node_summaries` and `generate_doc_description` helpers repeatedly call `asyncio.run` and create a new completion client per node/description; consider making the indexing flow async or batching calls to reuse a single client and event loop for better performance.
- In `generate_doc_description`, you currently pass `str(nodes)` to the model, which will include full Pydantic representations; it may be more efficient and predictable to serialize only the fields the model needs (e.g., titles, spans, and summaries) before sending to the LLM.
- The page slicing logic in `generate_node_summaries` (`doc.pages[node.start_index - 1 : node.end_index]`) relies on 1-based indices with an inclusive end; consider encapsulating this in a dedicated helper or adding explicit comments to avoid off-by-one mistakes in future changes.
## Individual Comments
### Comment 1
<location path="agentree/indexing/pdf_index.py" line_range="142" />
<code_context>
+ """
+ # Generate node summaries.
+ for node in nodes:
+ node_text = ''.join([page.content for page in doc.pages[node.start_index - 1 : node.end_index]])
+ # TODO: Optionally retain truncated node text (e.g. first 1000 chars) on node.text.
+ # node.text = node_text[:1000]
</code_context>
<issue_to_address>
**issue (bug_risk):** Potential off-by-one error in page slicing for node_text.
If `start_index`/`end_index` are meant to be 1-based inclusive page indices, this slice will drop the last page because the slice end is exclusive. In that case you’d need `doc.pages[node.start_index - 1 : node.end_index + 1]`. Please confirm whether `end_index` is inclusive or exclusive and adjust the slicing to match, so node summaries aren’t silently truncated.
</issue_to_address>
### Comment 2
<location path="agentree/indexing/pdf_index.py" line_range="146-147" />
<code_context>
+ # TODO: Optionally retain truncated node text (e.g. first 1000 chars) on node.text.
+ # node.text = node_text[:1000]
+ # TODO: Move this completion into a dedicated generation helper.
+ summary: NodeSummary = asyncio.run(
+ create_completion_client().complete(
+ node_text, NodeSummary, system_prompt=GENERATE_NODE_SUMMARY_PROMPT
+ )
</code_context>
<issue_to_address>
**suggestion (performance):** Repeated asyncio.run calls inside generate_node_summaries can be inefficient and brittle.
Each node (and its children) currently creates a new event loop via `asyncio.run`, which will scale poorly on large trees and prevents use from existing async code. Please factor this into an async helper (e.g. `async_generate_node_summaries`) that runs under a single event loop, and have sync callers invoke it once via `asyncio.run` at the top level so you can more easily support batching/pipelining later.
</issue_to_address>
### Comment 3
<location path="agentree/mcp/server.py" line_range="22-31" />
<code_context>
+
+
+@mcp.tool
+def index_pdf(path: str) -> str:
+ """Index a PDF file into a nested section Tree.
+
+ Args:
+ path: The path to the PDF file to index.
+
+ Returns:
+ The indexed Tree.
+ """
+ return str(index(path))
+
+
</code_context>
<issue_to_address>
**issue (bug_risk):** index_pdf returns a stringified Tree, which may be an awkward format for MCP clients and mismatches the docstring.
The implementation currently returns `str(index(path))`, while the docstring promises `The indexed Tree`. For a Pydantic `Tree`, `str(tree)` is a repr-like string rather than structured JSON, which makes it harder for MCP clients to consume. Consider returning the actual `Tree` or a structured JSON form (e.g. `index(path).model_dump()` / `model_dump_json()`) so the return type matches the docstring and provides a predictable schema for clients.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| """ | ||
| # Generate node summaries. | ||
| for node in nodes: | ||
| node_text = ''.join([page.content for page in doc.pages[node.start_index - 1 : node.end_index]]) |
There was a problem hiding this comment.
issue (bug_risk): Potential off-by-one error in page slicing for node_text.
If start_index/end_index are meant to be 1-based inclusive page indices, this slice will drop the last page because the slice end is exclusive. In that case you’d need doc.pages[node.start_index - 1 : node.end_index + 1]. Please confirm whether end_index is inclusive or exclusive and adjust the slicing to match, so node summaries aren’t silently truncated.
| summary: NodeSummary = asyncio.run( | ||
| create_completion_client().complete( |
There was a problem hiding this comment.
suggestion (performance): Repeated asyncio.run calls inside generate_node_summaries can be inefficient and brittle.
Each node (and its children) currently creates a new event loop via asyncio.run, which will scale poorly on large trees and prevents use from existing async code. Please factor this into an async helper (e.g. async_generate_node_summaries) that runs under a single event loop, and have sync callers invoke it once via asyncio.run at the top level so you can more easily support batching/pipelining later.
| def index_pdf(path: str) -> str: | ||
| """Index a PDF file into a nested section Tree. | ||
|
|
||
| Args: | ||
| path: The path to the PDF file to index. | ||
|
|
||
| Returns: | ||
| The indexed Tree. | ||
| """ | ||
| return str(index(path)) |
There was a problem hiding this comment.
issue (bug_risk): index_pdf returns a stringified Tree, which may be an awkward format for MCP clients and mismatches the docstring.
The implementation currently returns str(index(path)), while the docstring promises The indexed Tree. For a Pydantic Tree, str(tree) is a repr-like string rather than structured JSON, which makes it harder for MCP clients to consume. Consider returning the actual Tree or a structured JSON form (e.g. index(path).model_dump() / model_dump_json()) so the return type matches the docstring and provides a predictable schema for clients.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
agentree/indexing/pdf_index.py (2)
48-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDead initial-outline assignment now that the TOC branch is disabled.
outline: Outline = Outline(sections=[])at line 48 is never read before being overwritten at line 76, now that the TOC-found branch (which used to assignoutlineconditionally) is commented out. This is minor cleanup, not a functional bug, and can be addressed whenever the commented TOC path is revisited.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@agentree/indexing/pdf_index.py` around lines 48 - 96, Remove the unused initial `outline: Outline = Outline(sections=[])` assignment from the document indexing flow, since `extract_outline_initial` now always assigns `outline` before it is read. Leave the later outline assembly and continuation handling unchanged.
158-171: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse structured JSON instead of
str(nodes)for the document-description prompt.
str(nodes)sends a Pythonrepr()of theNodelist, not structured JSON. Elsewhere in this file (lines 78-80, 87-90), outlines are serialized via.model_dump_json(indent=2)before being logged or sent. Use the same approach here for a cleaner, more consistent, and more reliably parseable prompt input.♻️ Proposed fix
return asyncio.run( create_completion_client().complete( - str(nodes), DocumentDescription, system_prompt=GENERATE_DOC_DESCRIPTION_PROMPT + '\n'.join(node.model_dump_json(indent=2) for node in nodes), + DocumentDescription, + system_prompt=GENERATE_DOC_DESCRIPTION_PROMPT, ) )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@agentree/indexing/pdf_index.py` around lines 158 - 171, Update generate_doc_description to serialize the nodes input with each Node’s model_dump_json(indent=2) approach instead of passing str(nodes) to complete, matching the structured outline serialization used elsewhere in the file while preserving the existing prompt and response handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@agentree/indexing/pdf_index.py`:
- Around line 133-156: The generate_node_summaries flow must enforce
MAX_TOKENS_PER_CHUNK before sending node_text to complete(), reusing the
existing token-aware chunking or applying the established guard without changing
summary behavior. Also refactor the per-node asyncio.run calls in
generate_node_summaries so sibling summary requests share one event loop and can
execute concurrently, while preserving recursive child-summary generation.
In `@agentree/mcp/server.py`:
- Around line 21-31: Update the index_pdf tool to serialize the indexed Tree
with its Pydantic JSON serialization method instead of wrapping index(path) in
str(). Preserve the existing indexing flow and return the result of
index(path).model_dump_json() so MCP clients receive structured, parseable JSON.
- Around line 1-12: Update the module docstring for the server startup path to
accurately describe FastMCP’s default stdio transport: remove the claim that it
starts on port 8000 and document the command as using stdio, unless the
`mcp.run()` invocation is explicitly changed to an HTTP/streamable transport
with host and port configuration.
---
Nitpick comments:
In `@agentree/indexing/pdf_index.py`:
- Around line 48-96: Remove the unused initial `outline: Outline =
Outline(sections=[])` assignment from the document indexing flow, since
`extract_outline_initial` now always assigns `outline` before it is read. Leave
the later outline assembly and continuation handling unchanged.
- Around line 158-171: Update generate_doc_description to serialize the nodes
input with each Node’s model_dump_json(indent=2) approach instead of passing
str(nodes) to complete, matching the structured outline serialization used
elsewhere in the file while preserving the existing prompt and response
handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d96e5aa7-fe63-468e-882d-19cf50f1822f
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (10)
agentree/cli.pyagentree/indexing/pdf_index.pyagentree/indexing/prompts.pyagentree/mcp/__init__.pyagentree/mcp/server.pyagentree/models/nodes.pyagentree/models/tree.pyjustfilepyproject.tomltests/test_models.py
💤 Files with no reviewable changes (1)
- agentree/cli.py
| def generate_node_summaries(nodes: list[Node], doc: Document) -> None: | ||
| """Generate summaries for a list of nodes. | ||
|
|
||
| Args: | ||
| nodes: The nodes to generate summaries for. | ||
| doc: The document to generate summaries for. | ||
| """ | ||
| # Generate node summaries. | ||
| for node in nodes: | ||
| node_text = ''.join([page.content for page in doc.pages[node.start_index - 1 : node.end_index]]) | ||
| # TODO: Optionally retain truncated node text (e.g. first 1000 chars) on node.text. | ||
| # node.text = node_text[:1000] | ||
| # TODO: Move this completion into a dedicated generation helper. | ||
| summary: NodeSummary = asyncio.run( | ||
| create_completion_client().complete( | ||
| node_text, NodeSummary, system_prompt=GENERATE_NODE_SUMMARY_PROMPT | ||
| ) | ||
| ) | ||
| node.summary = summary.summary | ||
| logger.bind(node_id=node.id, node_summary=node.summary).info('Generated node summary') | ||
| # Generate summaries for the node's children. | ||
| if node.children: | ||
| generate_node_summaries(nodes=node.children, doc=doc) | ||
|
|
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
Add a token budget guard before sending node_text to the completion client.
generate_node_summaries concatenates every page in a node's range and sends it as a single prompt, with no size check. chunk_pages_with_overlap exists specifically because the outline-extraction prompt must stay under MAX_TOKENS_PER_CHUNK; a single large top-level section here can hit the same overflow the chunking logic was built to avoid, and since summaries are generated once per ancestor level, the same pages get resent multiple times. For a large document, this can exceed the completion client's context limit or produce degraded summaries.
Reuse the existing token-aware chunking (or at least assert/truncate node_text against MAX_TOKENS_PER_CHUNK) before calling complete().
Separately, each node's summary is generated with its own asyncio.run() call inside a for loop, so summaries are produced strictly serially with a new event loop per call. Consider batching these with a single asyncio.run(asyncio.gather(...)) (or making generate_node_summaries itself async) so sibling summaries can run concurrently instead of one request at a time.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@agentree/indexing/pdf_index.py` around lines 133 - 156, The
generate_node_summaries flow must enforce MAX_TOKENS_PER_CHUNK before sending
node_text to complete(), reusing the existing token-aware chunking or applying
the established guard without changing summary behavior. Also refactor the
per-node asyncio.run calls in generate_node_summaries so sibling summary
requests share one event loop and can execute concurrently, while preserving
recursive child-summary generation.
| """Agentree MCP server. | ||
|
|
||
| This server provides an MCP endpoint for indexing PDF files into a nested section Tree. | ||
|
|
||
| It can be run with: | ||
|
|
||
| ```bash | ||
| python -m agentree.mcp.server | ||
| ``` | ||
|
|
||
| It will start an MCP server on port 8000. | ||
| """ |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Does fastmcp (jlowin/fastmcp) version 3.4.5 FastMCP.run() default to stdio transport when called with no arguments?
💡 Result:
Yes, in FastMCP (including version 3.4.5), the FastMCP.run method defaults to the stdio transport when called with no arguments [1][2][3]. When you invoke mcp.run without specifying a transport, the server automatically communicates through standard input and output streams, which is the standard configuration for MCP clients such as Claude Desktop [1][2][3]. If you need to use a different transport, such as HTTP, you must explicitly specify it in the method call, for example: mcp.run(transport="http", port=8000) [4][1][2].
Citations:
- 1: https://github.com/jlowin/fastmcp/blob/a52ab0e9/docs/deployment/running-server.mdx
- 2: https://gofastmcp.com/deployment/running-server
- 3: https://fastmcp.wiki/en/deployment/running-server
- 4: https://github.com/jlowin/fastmcp/blob/main/docs/getting-started/quickstart.mdx
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== repo files =="
git ls-files | rg '(^pyproject\.toml$|^justfile$|agentree/mcp/server\.py|agentree/mcp|console)' || true
echo
echo "== server.py =="
if python3 - <<'PY'
import pathlib
p=pathlib.Path('agentree/mcp/server.py')
print(p.exists())
PY
then
nl -ba agentree/mcp/server.py | sed -n '1,80p'
fi
echo
echo "== pyproject dependency hints =="
if [ -f pyproject.toml ]; then
nl -ba pyproject.toml | sed -n '1,220p'
fi
echo
echo "== console script / script entrypoints =="
rg -n --hidden --glob '!*.lock' --glob '!dist/**' --glob '!build/**' 'fastmcp|mcp\.run|mcp\.write|agentree\.mcp|8000|transport' . || trueRepository: OctavianTocan/agentree
Length of output: 312
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== server.py first 80 lines =="
awk '{printf "%4d\t%s\n", NR, $0}' agentree/mcp/server.py | sed -n '1,80p'
echo
echo "== pyproject dependency and tooling hints =="
awk '{printf "%4d\t%s\n", NR, $0}' pyproject.toml | sed -n '1,220p'
echo
echo "== justfile =="
awk '{printf "%4d\t%s\n", NR, $0}' justfile
echo
echo "== relevant usages of FastMCP/MCP/server entrypoint/port =="
rg -n --hidden --glob '!*.lock' --glob '!dist/**' --glob '!build/**' 'FastMCP|mcp\.run|mcp\.write|agentree\.mcp|8000|transport|python -m agentree\.mcp\.server' . || trueRepository: OctavianTocan/agentree
Length of output: 9100
Fix the port/run docstring in agentree/mcp/server.py.
mcp.run() uses the FastMCP stdio transport by default, so neither python -m agentree.mcp.server nor the mcp script passes through a TCP port now. Either update the documented command/document that it uses stdio, or pass an HTTP/streamable transport with host/port if port 8000 is needed.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@agentree/mcp/server.py` around lines 1 - 12, Update the module docstring for
the server startup path to accurately describe FastMCP’s default stdio
transport: remove the claim that it starts on port 8000 and document the command
as using stdio, unless the `mcp.run()` invocation is explicitly changed to an
HTTP/streamable transport with host and port configuration.
| @mcp.tool | ||
| def index_pdf(path: str) -> str: | ||
| """Index a PDF file into a nested section Tree. | ||
|
|
||
| Args: | ||
| path: The path to the PDF file to index. | ||
|
|
||
| Returns: | ||
| The indexed Tree. | ||
| """ | ||
| return str(index(path)) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Return structured JSON instead of a Python repr string.
index_pdf returns str(index(path)). Pydantic's default str() produces a repr-style string (e.g., doc_name='x.pdf' doc_description=... nodes=[...]), not JSON. For a tool whose docstring promises "The indexed Tree," return tree.model_dump_json() so MCP clients and agents receive a structured, reliably parseable payload consistent with how the rest of the codebase serializes Tree/Outline objects (e.g., agentree/indexing/pdf_index.py uses .model_dump_json(indent=2)).
🔧 Proposed fix
def index_pdf(path: str) -> str:
"""Index a PDF file into a nested section Tree.
Args:
path: The path to the PDF file to index.
Returns:
The indexed Tree.
"""
- return str(index(path))
+ return index(path).model_dump_json()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @mcp.tool | |
| def index_pdf(path: str) -> str: | |
| """Index a PDF file into a nested section Tree. | |
| Args: | |
| path: The path to the PDF file to index. | |
| Returns: | |
| The indexed Tree. | |
| """ | |
| return str(index(path)) | |
| `@mcp.tool` | |
| def index_pdf(path: str) -> str: | |
| """Index a PDF file into a nested section Tree. | |
| Args: | |
| path: The path to the PDF file to index. | |
| Returns: | |
| The indexed Tree. | |
| """ | |
| return index(path).model_dump_json() |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@agentree/mcp/server.py` around lines 21 - 31, Update the index_pdf tool to
serialize the indexed Tree with its Pydantic JSON serialization method instead
of wrapping index(path) in str(). Preserve the existing indexing flow and return
the result of index(path).model_dump_json() so MCP clients receive structured,
parseable JSON.
Introduced a new `.mcp.json` file to configure the MCP server for the agentree project, enabling the use of the `uv` command to run the agentree server with specified arguments. Additionally, updated the TODO.md to mark tasks related to exposing per-page text and generating document descriptions as complete. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.mcp.json:
- Around line 5-12: Update the MCP command configuration to remove the
hard-coded /mnt/work/code/personal/agentree project path, use a portable
project-root or environment-specific setting, and invoke the registered mcp
entrypoint instead of the module path when that is the supported packaging
contract.
In `@TODO.md`:
- Around line 45-48: Keep the per-page text exposure task incomplete until
implemented. Update the indexing contract around index() to return
Document.pages alongside the Tree, then propagate and persist those pages
through storage and expose them via get_page_content; update the CLI
accordingly, or leave the TODO unchecked if this contract is not implemented.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f474be38-c7d0-4141-b4c6-27bbcf6cfa05
📒 Files selected for processing (2)
.mcp.jsonTODO.md
| "command": "uv", | ||
| "args": [ | ||
| "run", | ||
| "--project", | ||
| "/mnt/work/code/personal/agentree", | ||
| "python", | ||
| "-m", | ||
| "agentree.mcp.server" |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Remove the machine-specific project path.
/mnt/work/code/personal/agentree will not exist for other developers or MCP clients. Use a portable project-root configuration or an environment-specific setting, and invoke the registered mcp entrypoint when that is the supported packaging contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.mcp.json around lines 5 - 12, Update the MCP command configuration to
remove the hard-coded /mnt/work/code/personal/agentree project path, use a
portable project-root or environment-specific setting, and invoke the registered
mcp entrypoint instead of the module path when that is the supported packaging
contract.
| - [x] **Expose per-page text from `index()`** — `Document.pages` already | ||
| holds tagged pages; they never leave `index()`. Storage and | ||
| `get_page_content` need them. Return pages alongside the tree (or a | ||
| small result type). (`agentree/indexing/pdf_index.py`) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Do not mark per-page text exposure complete yet.
agentree/indexing/pdf_index.py still returns only Tree, and Document.pages remain local to index(). The CLI also persists only the tree. Either implement a result contract that carries the pages through storage and get_page_content, or keep this task open.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@TODO.md` around lines 45 - 48, Keep the per-page text exposure task
incomplete until implemented. Update the indexing contract around index() to
return Document.pages alongside the Tree, then propagate and persist those pages
through storage and expose them via get_page_content; update the CLI
accordingly, or leave the TODO unchecked if this contract is not implemented.
Summary
Agents can index a PDF through MCP (
index_pdf) without going through the Typer CLI, and the assembled tree now carries per-section summaries plus a document-level description so downstream retrieval can work from the tree alone.After outline assembly,
index()enriches nodes and then the document;assemble_treestays structural so unit tests do not hit the LLM. Console entrypoints are split:clifor the indexer,mcpfor the FastMCP server (just mcp/uv run mcp).Note: the TOC-found indexing path is temporarily commented out, so every PDF currently takes the no-TOC chunked outline path. The local
.mcp.jsonwith a host-absolute project path was left uncommitted.Validation
uv run ruff format --check agentree testsuv run ruff check agentree testsuv run ty check agentree testsuv run pytest -q— 45 passed, 3 skipped (live Claude gates)New concepts
FastMCP
What it is. FastMCP is a Python framework for building MCP servers: you declare tools as ordinary functions, and it handles the MCP protocol over stdio (or HTTP).
Why here. Agentree already depended on the MCP SDK; FastMCP is the shortest path to a working
index_pdftool without hand-rolling protocol wiring. The alternative was a raw MCP server with more boilerplate for the same one-tool surface.Example from this PR.
agentree/mcp/server.pyregistersindex_pdf(path) -> str, which calls the existingindex()pipeline and returns the tree string.When not to use it. Skip FastMCP when you need a custom transport or protocol behavior the framework does not expose cleanly — then drop to the lower-level MCP SDK.
Summary by Sourcery
Add MCP-based PDF indexing server and enrich assembled trees with per-node summaries and a document-level description for downstream retrieval.
New Features:
Enhancements:
Build:
Tests:
Summary by CodeRabbit
New Features
Improvements