Multi-file server¶
Who this page is for: someone whose MCP server is split across several Python files.
What you will learn¶
- How mcpolish walks a directory.
- How tools spread across files get merged into one report.
- Which folders mcpolish skips automatically.
- How to point mcpolish at the project root.
Background¶
Most real MCP servers grow beyond a single file. A main.py creates the FastMCP instance, and tools live under tools/. mcpolish handles this layout directly: pass the directory and it walks every .py file.
Step by step¶
1. Lay out the project¶
my_mcp_server/
pyproject.toml
src/
my_mcp_server/
__init__.py
main.py FastMCP("my_server") lives here
tools/
__init__.py
search.py @mcp.tool() definitions
memory.py @mcp.tool() definitions
admin.py @mcp.tool() definitions
helpers/
util.py no tools, just helpers
tests/
2. main.py¶
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my_server")
from . import tools # noqa: F401 trigger the decorators
3. tools/init.py¶
4. tools/search.py¶
from ..main import mcp
@mcp.tool()
def search_records(query: str, limit: int = 10) -> list:
"""Use this when the user wants to find records by keyword.
Args:
query: Search terms. Example: "kubernetes".
limit: Max number of results.
Returns: a list of matching records.
"""
return []
5. Lint the whole project¶
mcpolish walks every .py file in the directory. It finds:
- The
FastMCP("my_server")constructor inmain.py, setting the namespace. - One tool in
tools/search.py. - Two tools in
tools/memory.py. - One tool in
tools/admin.py. - Zero tools in
helpers/util.py(walked but contributes nothing).
The report names every diagnostic with its original file and line number. A diagnostic from tools/admin.py is reported there, not in main.py.
What gets skipped¶
mcpolish skips these directory names anywhere in the path:
So your virtualenv and build artifacts do not get linted.
Cross-file rules¶
Three rules look at the merged set of tools, not one file at a time:
- MP011 redundant-prefix: uses the server namespace detected anywhere in the project.
- MP012 inconsistent-verb-pattern: compares verbs across every tool in every file.
- MP033 duplicate-tool-description: catches two tools that share a description even if they live in different files.
Example: tools/search.py has get_record, tools/memory.py has fetch_note. MP012 fires because the canonical verb across the project is get, and fetch is an outlier.
Common variations¶
Linting only one subfolder¶
mcpolish builds the registry from that subfolder only. Cross-file rules still work but only across files within the subfolder. The namespace defaults to the basename of the path if no FastMCP("name") call is visible.
Setting the namespace manually¶
If you split your project in unusual ways and mcpolish does not detect the namespace, set it in pyproject.toml:
This applies to every run.
Running from a parent directory¶
Works the same as cd my_mcp_server && mcpolish lint .. The reported file paths are relative to where you ran the command.
Troubleshooting¶
Discovered fewer tools than expected. Some patterns are skipped:
- Dynamic registration in a loop (no static name literal). See dynamic_registration.py fixture.
- Tools registered behind a conditional import.
- Tools defined inside a function body.
mcpolish reads source statically. If a tool is not visible from the AST, it is invisible to mcpolish.
MP011 does not fire even though the namespace is clear. Check that FastMCP("name") (or Server("name")) is in one of the scanned files. If your namespace lives in a config file or environment variable, set it manually in pyproject.toml.