Skip to main content

MCPToolForge

A developer-friendly Python framework for creating and exposing Model Context Protocol (MCP) tools with minimal boilerplate.

Project Status

Active Development

MCPToolForge now supports exposing registered Python tools as Model Context Protocol (MCP) tools using the standard low-level Server over the process's standard input/output (stdio) streams. Network and SSE transports are planned for upcoming stages.

What MCPToolForge Solves

Exposing Python code as tools for AI/LLM systems currently requires complex boilerplate code or manual JSON Schema definitions. MCPToolForge simplifies this by:

  1. Inferring tool schemas automatically from Python function signatures and type annotations.
  2. Managing the registry, validation, and lifecycle of registered callables.
  3. Decoupling tool execution logic from specific transports.

Planned Architecture

MCPToolForge separates concerns into distinct modules:

  • MCPServer: Core API server orchestration.
  • registry: Tracks and maps tools to callables.
  • schema: Automatically converts function signatures to JSON schemas.
  • execution: Safely executes tools, handling sync and async functions.
  • transports: Implements JSON-RPC over stdio, SSE, or custom transport protocols.

Installation

pip install mcptoolforge

Basic Usage

from mcptoolforge import MCPServer

server = MCPServer("my-tools")


@server.tool
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

Creating a Tool

To register a tool on an MCPServer, use the @server.tool decorator:

from mcptoolforge import MCPServer

server = MCPServer("demo")


@server.tool
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

Internally, MCPToolForge handles this registration seamlessly:

  Python function
         ↓
   @server.tool
         ↓
   Tool metadata (name, docstring, parameters, return type)
         ↓
    ToolRegistry

Tool Metadata

MCPToolForge supports both simple and configured registration styles for tools. This allows you to attach custom names, descriptions, tags, and generic metadata while keeping the decoration interface clean and backward compatible.

Simple Decorator Style

By default, MCPToolForge infers the tool name from the Python function's name and the description from the docstring:

@server.tool
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

Configured Decorator Style

To specify custom metadata, pass arguments directly to the @server.tool(...) decorator:

@server.tool(
    name="calculator",
    description="Perform arithmetic calculations",
    tags=["math", "utility"],
    metadata={"version": "1.0.0"},
)
def calculate(expression: str) -> float:
    """Original docstring is ignored in favor of custom description."""
    ...

Metadata Fields and Behaviors

  • name (str | None): Custom name exposed to MCP clients. It must be non-empty and match the regex ^[a-zA-Z_][a-zA-Z0-9_-]*$. If not provided, it defaults to the Python function name. Note that the Python function name and MCP tool name can differ (the function remains callable locally by its original name).
  • description (str | None): Custom description. Takes precedence over the function's docstring. If both are missing, a fallback description ("No description provided.") is used. Empty custom descriptions are rejected.
  • tags (list[str] | None): Optional list of non-empty strings representing tool categories/tags. Duplicate tags are automatically deduplicated while preserving order.
  • metadata (dict[str, Any] | None): Optional dictionary of custom JSON-serializable key-value metadata. Core fields (such as name, description, tags, etc.) are restricted from being overridden in this dictionary.
  • input_schema & parameters: Automatically generated from the Python signature type annotations. Schema generation remains unaffected by custom metadata.

Tool Immutability

Once a tool is registered, its core metadata (fn, name, description, parameters, return_type, tags, metadata) is protected against accidental modifications using read-only properties.


Resources

MCPToolForge supports exposing readable data/context through MCP Resources with a simple decorator-based API.

Difference Between Tools and Resources

  • Tools: AI models request the server to perform actions/operations (e.g. write to files, calculate values, send network requests).
  • Resources: AI models query the server to read information (e.g. configuration states, log entries, file contents).

Registering Resources

To register a resource, use the @server.resource(uri, ...) decorator:

@server.resource(
    "config://app",
    description="Application configuration parameters",
    mime_type="application/json",
)
def app_config():
    return {"name": "MCPToolForge", "version": "0.1.0"}

Resource URI

The URI serves as the unique identifier for the resource.

  • It must be a valid non-empty URI string containing a scheme and either a netloc or a path (e.g., config://app, file:///path/to/doc).
  • Duplicate URIs will raise ResourceAlreadyRegisteredError.

Resource Name and Description

  • name (str | None): Custom human-readable resource name. If not provided, it defaults to the Python function name.
  • description (str | None): Custom description of the resource. Takes precedence over the function docstring. If both are missing, defaults to "No description provided.".

MIME Type

  • mime_type (str | None): Optional MIME type of the returned content. If omitted and the resource returns a dictionary or list, it defaults to application/json. Otherwise, it remains unspecified.

Sync vs Async Resources

Both synchronous and asynchronous resource handler callables are supported natively:

@server.resource("log://active")
async def read_logs():
    return "Log file content..."

Result Serialization

Resources return contents mapped to the standard MCP model formats depending on the return type:

  • str: Mapped to TextResourceContents (retains text formatting).
  • dict | list: Automatically serialized to JSON and mapped to TextResourceContents (defaults to application/json mime type).
  • bytes: Base64 encoded and mapped to BlobResourceContents (binary format).
  • Other types: If the return type is not supported, ResourceExecutionError is raised.

[!NOTE] MCPToolForge is in early development. Standard Model Context Protocol (MCP) transport support (such as stdio JSON-RPC or Server-Sent Events) is upcoming. Currently, registration and tool introspection function locally.

Automatic Schema Generation

MCPToolForge automatically derives JSON Schema inputs from standard Python type annotations and default values. Developers do not need to write JSON schemas manually.

For example, given this tool:

@server.tool
def search(query: str, limit: int = 10):
    """Search information."""
    ...

MCPToolForge dynamically generates the corresponding input schema:

{
  "type": "object",
  "properties": {
    "query": {
      "type": "string"
    },
    "limit": {
      "type": "integer",
      "default": 10
    }
  },
  "required": ["query"]
}

This ensures full type safety and seamless integration with MCP clients out-of-the-box.

Runtime Validation

MCPToolForge verifies incoming arguments against the generated tool schemas during execution. If invalid, missing, or unexpected arguments are supplied, the error is isolated and returned cleanly without crashing the server.

  Tool registration
         ↓
  Schema generation
         ↓
  Runtime argument validation (type safety checks)
         ↓
  Tool execution (sync or async)

For example:

  • Valid Call: add(10, 20) $\to$ returns 30.
  • Missing Parameter: Calling add(10) $\to$ returns error: Tool 'add': missing required parameter 'b'.
  • Invalid Parameter Type: Calling add("hello", 20) $\to$ returns error: Tool 'add': parameter 'a' expected integer, received str.
  • Unexpected Parameter: Calling add(10, 20, c=30) $\to$ returns error: Tool 'add': unexpected parameter 'c'.

MCP Server

MCPToolForge handles the translation and registration of local Python callables as standard Model Context Protocol (MCP) tools:

 @server.tool
       ↓
 Tool registration (ToolRegistry)
       ↓
 Schema generation (input_schema)
       ↓
 MCP tool exposure (MCPAdapter maps schema and name)
       ↓
 Stdio server execution (MCPServer.run() processes requests)

Run locally

An example MCP server exposing add and greet tools is included at basic_mcp_server.py.

To start this server over the standard input/output (STDIO) transport:

python examples/basic_mcp_server.py

You can connect to this server using any standard MCP client or command-line inspector (such as @modelcontextprotocol/inspector).

End-to-End MCP Verification

MCPToolForge's MCP implementation is verified end-to-end through automated integration tests that simulate a complete client-server conversation:

  1. MCP Initialization Handshake: Establishes protocol compatibility and negotiates capabilities.
  2. Tool Discovery (list_tools): Allows client to discover registered tools (add, greet, failing_tool, get_info), mapping schemas, types, and descriptions accurately.
  3. Tool Invocations (call_tool): Runs synchronous/asynchronous callables natively and returns outputs (structured data for dict results, and TextContent blocks for other primitive returns).
  4. Error Isolation: Confirms execution exceptions or invalid parameters return cleanly as error results (is_error=True) without crashing the active server.

To execute the verification suite:

python3 -m pytest tests/integration/test_mcp_server.py

Command-Line Interface (CLI)

MCPToolForge includes a developer-friendly command-line interface to create, run, and inspect servers with minimal setup:

Installation

Ensure the package is installed:

pip install mcptoolforge

Usage and Help

To view all available commands:

mcptoolforge --help

Initialize a Project

Create a new working MCP server template in a specified directory:

mcptoolforge init my-server

This generates the following folder structure:

my-server/
├── server.py
├── pyproject.toml
├── README.md
└── .gitignore

Note: If files already exist in the target directory, init will fail safely to prevent accidental overwrites. Use --force to overwrite.

List Tools

Display all registered tools on a server file without starting the transport loops:

mcptoolforge list --file server.py

Inspect Server and Tool Schemas

Inspect general server details:

mcptoolforge inspect --file server.py

Inspect details and input schemas of a specific tool:

mcptoolforge inspect add --file server.py

Run Server

Start the MCP transport loops for standard input/output (STDIO) transport:

mcptoolforge run --file server.py

Configuration

MCPToolForge projects are configured inside the project's standard pyproject.toml file under the [tool.mcptoolforge] section. This enables automatic project discovery and streamlines development.

Configuration Format

Here is an example configuration block:

[tool.mcptoolforge]
name = "my-server"
entrypoint = "server.py"
transport = "stdio"

Configuration Fields

  • name (required): The name of your MCP server.
  • entrypoint (optional, default: "server.py"): The relative path to the Python file containing your MCPServer instance.
  • transport (optional, default: "stdio"): The transport protocol to use. Currently, only "stdio" is supported. Specifying other transport mechanisms (e.g. "http") will produce an error.

Project Discovery & Resolving Root

When you run commands like mcptoolforge run, mcptoolforge list, or mcptoolforge inspect without passing an explicit --file argument:

  1. MCPToolForge starts search from the current working directory (Path.cwd()).
  2. It climbs up parent directories looking for a pyproject.toml containing a [tool.mcptoolforge] section.
  3. The directory containing pyproject.toml is resolved as the Project Root. All relative paths (e.g., entrypoint) are resolved relative to this root.

Entrypoint & Naming Convention

When loading the entrypoint module, MCPToolForge looks for a variable named server that is an instance of MCPServer.

  • If the variable server is missing, or if it is not an instance of MCPServer, an error is raised.
  • If multiple MCPServer instances exist in the entrypoint file and none is named server, MCPToolForge raises an ambiguity error.

CLI Precedence & Overrides

MCPToolForge resolves the server to load using the following order of precedence:

  1. Explicit CLI arguments (e.g., mcptoolforge run --file custom_server.py)
  2. Configuration values defined in pyproject.toml
  3. MCPToolForge defaults (server.py in current working directory)

Simplified Workflow

With configuration and project discovery in place, you can run and inspect servers with zero boilerplate:

# 1. Initialize a new project (includes configuration automatically)
mcptoolforge init my-server
cd my-server

# 2. Inspect the project details and tools
mcptoolforge inspect

# 3. List the registered tools
mcptoolforge list

# 4. Start the server using stdio transport
mcptoolforge run

Middleware

MCPToolForge supports a powerful middleware pipeline that allows you to run cross-cutting concerns (logging, timing, tracing, error handling) around tool execution without modifying your individual tools.

Defining Middleware

To define a middleware, use the @server.middleware decorator or register it programmatically via server.add_middleware().

import logging
from mcptoolforge import MCPServer

logger = logging.getLogger("my_app")
server = MCPServer("my-server")


# Asynchronous middleware
@server.middleware
async def custom_logger(context, next_callable):
    logger.info(f"Invoking {context.tool_name} with args: {context.arguments}")
    try:
        result = await next_callable()
        logger.info(f"Successfully finished {context.tool_name}")
        return result
    except Exception as e:
        logger.error(f"Tool {context.tool_name} failed: {e}")
        raise

[!CAUTION] STDIO Logging Safety: Because standard output (stdout) is reserved for standard MCP protocol communication over the STDIO transport, never print to stdout inside middleware (e.g. do not use print()). Always write logs to standard error (stderr) using Python's logging module or sys.stderr.

Middleware Execution Order

Middlewares execute in the order they are registered:

    Middleware A (before)
        ↓
    Middleware B (before)
        ↓
    Validation & Tool Execution
        ↓
    Middleware B (after)
        ↓
    Middleware A (after)

Sync vs Async Middleware

MCPToolForge supports both synchronous and asynchronous middlewares:

  • Async Middleware: async def middleware(context, next_callable): ... — works with both sync and async tools.
  • Sync Middleware: def middleware(context, next_callable): ... — works with synchronous tools.
  • Mixed Safety: To prevent blocking or fragile event loop bridging, a synchronous middleware cannot wrap an asynchronous tool or another asynchronous middleware. Violations will raise a ConfigurationError.

Built-in Middlewares

MCPToolForge includes pre-packaged middlewares for common workflows:

  • Timing: Measures and logs tool execution duration to stderr (timing_middleware for async pipelines, sync_timing_middleware for purely sync pipelines).
  • Logging: Traces tool parameters, entry, and exit statuses (logging_middleware for async pipelines, sync_logging_middleware for purely sync pipelines).
from mcptoolforge import MCPServer, timing_middleware, logging_middleware

server = MCPServer("demo")
server.add_middleware(logging_middleware)
server.add_middleware(timing_middleware)

Middleware Context

The MiddlewareContext object provides the following attributes to inspect tool execution state:

  • context.tool_name (str): Name of the tool being executed.
  • context.tool (Tool): The registered MCPToolForge Tool object.
  • context.arguments (dict): Mutatable inputs passed to the tool.
  • context.server (MCPServer): The active server instance.
  • context.duration (float | None): MONOTONIC execution time recorded by timing frameworks.
  • context.error (Exception | None): Captured exception if execution failed.

Lifecycle Hooks

You can register startup and shutdown lifecycle hooks on your MCPServer. These hooks run at server initialization and teardown points (e.g. for database initialization or cleanup).

@server.on_startup
async def db_init():
    logger.info("Initializing database...")


@server.on_shutdown
def db_cleanup():
    logger.info("Cleaning up connections...")

Startup and shutdown hooks support both synchronous and asynchronous functions and will be executed sequentially in the order of registration.


Testing

MCPToolForge provides a first-class in-process testing client, MCPTestClient, which allows developers to test their tools, resources, and prompts locally without running Claude, Cursor, ChatGPT, or an external MCP client.

Basic Usage

The MCPTestClient operates directly against your MCPServer instance:

from mcptoolforge import MCPServer
from mcptoolforge.testing import MCPTestClient

server = MCPServer("demo")


@server.tool
def add(a: int, b: int) -> int:
    return a + b


def test_add():
    # Sync client usage
    client = MCPTestClient(server)
    result = client.call_tool("add", {"a": 10, "b": 20})
    assert result == 30

Lifecycle Hooks

If your server configures startup or shutdown hooks, you can use the test client as a context manager to trigger them automatically:

def test_lifecycle():
    with MCPTestClient(server) as client:
        # startup hooks have run
        assert len(client.list_tools()) == 1
    # shutdown hooks have run

For async tests, use the async context manager:

async def test_lifecycle_async():
    async with MCPTestClient(server) as client:
        result = await client.call_tool_async("add", {"a": 1, "b": 2})
        assert result == 3

Testing Resources

To list and read registered resources:

def test_resources():
    client = MCPTestClient(server)

    # List resources
    resources = client.list_resources()
    assert len(resources) == 1
    assert resources[0].uri == "config://app"

    # Read resource
    res_data = client.read_resource("config://app")
    assert "MCPToolForge" in res_data.text
    assert res_data.mime_type == "application/json"

Testing Prompts

To list and retrieve prompts:

def test_prompts():
    client = MCPTestClient(server)

    # List prompts
    prompts = client.list_prompts()
    assert len(prompts) == 1
    assert prompts[0].name == "explain"

    # Get prompt
    prompt_data = client.get_prompt("explain", {"topic": "MCP"})
    assert len(prompt_data.messages) == 1
    assert prompt_data.messages[0].content == "Explain MCP in simple terms."
    assert prompt_data.messages[0].role == "user"

Testing Middleware

Middleware layers are executed automatically when invoking tools via the MCPTestClient, allowing you to assert that custom logging, timings, or authorization middleware behaves correctly:

def test_middleware():
    events = []

    @server.middleware
    def my_middleware(context, next_fn):
        events.append("before")
        res = next_fn()
        events.append("after")
        return res

    client = MCPTestClient(server)
    client.call_tool("add", {"a": 1, "b": 2})
    assert events == ["before", "after"]

Development Setup

  1. Create and activate a virtual environment:

    python3 -m venv .venv
    source .venv/bin/activate
    
  2. Install the package in editable mode:

    pip install -e .
    
  3. Install development dependencies:

    pip install pytest ruff
    

Testing Command

To run the unit tests:

pytest

Roadmap

  • Foundation architecture layout and schema generation
  • Implement local stdio JSON-RPC transport
  • Add CLI interface for installing and executing servers
  • Implement robust runtime argument validation and tool execution
  • Implement SSE transport for network integration
  • Integration with MCP-compliant AI environments (e.g. Claude Desktop)

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mcptoolforge-0.1.0.tar.gz (48.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mcptoolforge-0.1.0-py3-none-any.whl (41.2 kB view details)

Uploaded Python 3

File details

Details for the file mcptoolforge-0.1.0.tar.gz.

File metadata

  • Download URL: mcptoolforge-0.1.0.tar.gz
  • Upload date:
  • Size: 48.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for mcptoolforge-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c59174f3936f4d1f110c00ede371a1a0cd4c8615d8c52439098892ded89ef384
MD5 c1ebb9e97e0f2ce9b455f8249b55f6bb
BLAKE2b-256 05f8381088b8b664104421c02b88466e4df884b0d8480cd1e823a8afa5924419

See more details on using hashes here.

Provenance

The following attestation bundles were made for mcptoolforge-0.1.0.tar.gz:

Publisher: publish.yml on Lakshyalamba/toolforge

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mcptoolforge-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: mcptoolforge-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 41.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for mcptoolforge-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6b3e3583b40fd0481773ca08645eaedee9ba51e1cfacfc800d9dffce0ff592b4
MD5 0b2ec2c0f804d10c9c35f4d89322ac9d
BLAKE2b-256 985e378c9f8569172ac4d090849f6a56a43ec6ceaa2ef1ec61716309164d9067

See more details on using hashes here.

Provenance

The following attestation bundles were made for mcptoolforge-0.1.0-py3-none-any.whl:

Publisher: publish.yml on Lakshyalamba/toolforge

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page