Skip to content

Conversation

@jamesev15
Copy link

Add Dependency Injection Support for MCP Tools

This PR introduces a comprehensive dependency injection system for MCP tools, enabling automatic resolution of function dependencies with support for sync/async functions, generators, and proper resource cleanup.

Motivation and Context

Currently, MCP tools have limited support for managing external dependencies like database connections, configuration objects, or shared resources. This leads to:

  • Tight coupling: Tools directly instantiate their dependencies, making them hard to test and modify
  • Poor testability: Difficult to mock dependencies for unit testing
  • Code duplication: Similar dependency setup repeated across multiple tools

This change introduces a FastAPI-inspired dependency injection system that solves these problems by providing:

  • Automatic dependency discovery and resolution
  • Type-safe dependency injection
  • Proper resource lifecycle management
  • Improved testability and maintainability

How Has This Been Tested?

  • Unit tests for dependency resolution with various function types
  • Unit tests with sync/async functions, generators, and async generators
  • Error handling tests to ensure proper cleanup on failures

Breaking Changes

None. This is a fully backward-compatible addition. Existing tools will continue to work without any modifications. The dependency injection system is opt-in and only activates when dependencies are explicitly declared using the Depends class.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling

Additional context

Implementation Details

Key Components:

  1. Depends class: Simple wrapper for dependency functions
  2. find_dependencies(): Automatically discovers dependencies in function signatures using type hints and parameter defaults
  3. DependencyResolver: Manages the lifecycle of dependencies with proper cleanup

Design Decisions:

  • FastAPI-inspired API: Uses familiar patterns for developers already using FastAPI
  • Type safety: Full support for Annotated types and type hints
  • Resource safety: Automatic cleanup of generator-based dependencies
  • Error resilience: If any dependency fails to resolve, all previously created resources are properly cleaned up
from mcp.server.fastmcp import FastMCP
from mcp.types import Depends
from typing import Annotated
from collections.abc import Generator, AsyncGenerator
from random import randint
import asyncio

mcp = FastMCP("Demo")

def load_resource() -> int:
    return randint(1, 10)


async def load_async_resource() -> str:
    await asyncio.sleep(0.1)
    return f"async_data_{randint(100, 999)}"


def database_connection() -> Generator[str, None, None]:
    connection_id = f"db_conn_{randint(1000, 9999)}"
    try:
        yield connection_id
    finally:
        pass


async def async_file_handler() -> AsyncGenerator[str, None]:
    file_id = f"file_{randint(1000, 9999)}"
    try:
        yield file_id
    finally:
        await asyncio.sleep(0.05)
        pass

# Tool 1: Regular function dependency
@mcp.tool()
def add_with_regular_dep(a: int, b: int, c: Annotated[int, Depends(dependency=load_resource)]) -> int:
    return a + b + c


# Tool 2: Async function dependency
@mcp.tool()
async def process_with_async_dep(text: str, async_data: Annotated[str, Depends(dependency=load_async_resource)]) -> str:
    return f"Processed '{text}' with {async_data}"


# Tool 3: Generator dependency (with cleanup)
@mcp.tool()
def query_database(query: str, db_conn: Annotated[str, Depends(dependency=database_connection)]) -> str:
    return f"Executed '{query}' on connection {db_conn}"


# Tool 4: Async generator dependency (with async cleanup)
@mcp.tool()
async def process_file(content: str, file_handler: Annotated[str, Depends(dependency=async_file_handler)]) -> str:
    await asyncio.sleep(0.1)
    return f"Processed content '{content}' with handler {file_handler}"


# Tool 5: Multiple dependencies
@mcp.tool()
async def complex_operation(
    data: str,
    regular_dep: Annotated[int, Depends(dependency=load_resource)],
    async_dep: Annotated[str, Depends(dependency=load_async_resource)],
    db_conn: Annotated[str, Depends(dependency=database_connection)],
    file_handler: Annotated[str, Depends(dependency=async_file_handler)],
) -> str:
    result = (
        f"Complex operation on '{data}' using:\n"
        f"- Regular resource: {regular_dep}\n"
        f"- Async resource: {async_dep}\n"
        f"- DB connection: {db_conn}\n"
        f"- File handler: {file_handler}"
    )
    return result

@felixweinberger felixweinberger added enhancement New feature or request needs maintainer action Potentially serious issue - needs proactive fix and maintainer attention labels Oct 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request needs maintainer action Potentially serious issue - needs proactive fix and maintainer attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants