Resolved

Python decorator makes functions non-callable with TypeError

Python
Decorators
Refactoring
Clean Architecture
Backend
AI/ML
TypeError
Migration

Functions wrapped with `@mcp.tool` decorator became non-callable `FunctionTool` objects | Clean architecture refactor required to separate business logic from MCP layer

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: httpx was 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.tool decorator 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:

  1. Content Layer: Handles URL/file fetching using urllib.request.
  2. Core Business Logic: Pure Python functions with no external dependencies.
  3. LLM Interface: Manages interactions with the OpenRouter API.
  4. Workflow Orchestration: Evaluator-optimizer pattern for LLM iterations.
  5. MCP Layer: Optional (disabled) for future server-mode implementation.

4. Dependency Reduction

  • Removed: httpx dependency.
  • 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.