Issue
Python functions decorated with @mcp.tool became non-callable, throwing TypeError: 'FunctionTool' object is not callable. This prevented direct function invocation and forced reliance on the MCP layer for execution. Additionally, the system lacked a content-fetching mechanism for URLs/files referenced in LLM prompts, leading to unreliable outputs and hallucinations. Secondary challenges included:
- Missing content layer: No implementation for pre-fetching URLs/files before LLM processing.
- Architecture coupling: Business logic and MCP decorator functionality were tightly intertwined.
- Dependency bloat: Unnecessary external dependencies (e.g.,
httpx) were introduced for simple HTTP operations.
Culprit
Decorator Problem
The @mcp.tool decorator wrapped functions in a FunctionTool class, which implemented the tool interface but removed direct callability. Functions decorated with @mcp.tool returned FunctionTool objects instead of callable functions, breaking expected behavior.
Content Fetching Gap
LLM prompts contained instructions to “fetch URLs” or “fetch files”, but no actual content-fetching logic was implemented. This forced the LLM to either:
- Hallucinate content, or
- Fail silently when real content was required.
Architecture Issues
- No separation of concerns: Business logic, content fetching, and MCP layer were intermixed, violating clean architecture principles.
- External dependency:
httpxwas used for HTTP operations, adding unnecessary complexity. - Missing layers: No clear preprocessing step for content before LLM calls, leading to inconsistent results.
Solution
1. Decorator Removal and Architecture Separation
Changes Made:
- Removed
@mcp.tooldecorator from business logic functions, restoring direct callability. - Separated business logic into pure Python functions.
- Disabled the MCP layer (commented out) to decouple logic from server dependencies.
Example Change:
# Before: Non-callable FunctionTool object
@mcp.tool
def generate_cover_letter():
...
# After: Directly callable function
def generate_cover_letter():
...
2. Content Fetching Implementation
Introduced a dedicated content layer using the standard library (urllib.request) to pre-fetch URLs/files before LLM processing. This eliminated hallucination risks and ensured reliable content injection into prompts.
3. Clean 5-Layer Architecture
Reorganized the system into distinct layers:
- Content Layer: Handles URL/file fetching using
urllib.request. - Core Business Logic: Pure Python functions with no external dependencies.
- LLM Interface: Manages interactions with the OpenRouter API.
- Workflow Orchestration: Evaluator-optimizer pattern for LLM iterations.
- MCP Layer: Optional (disabled) for future server-mode implementation.
4. Dependency Reduction
- Removed:
httpxdependency. - Added: None (used standard library).
- Result: Simplified codebase with no external HTTP dependencies.
Performance Outcomes
- Content fetching: Completed in under 5 seconds for 6 sources.
- LLM iterations:
- Cover letter: 2 iterations to achieve “EXCELLENT” rating.
- CV generation: 4 iterations to achieve “EXCELLENT” rating.
- Total execution: ~70 seconds (including LLM processing).
Generated Artifacts
cover_letter_sozialdienst_limmattal.md(rated “EXCELLENT”).cv_sozialdienst_limmattal.md(rated “EXCELLENT”).
Pending Work
- PDF conversion requires
weasyprint(unrelated to this refactor). - MCP layer remains commented out for future server-mode implementation.