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:
- Inferring tool schemas automatically from Python function signatures and type annotations.
- Managing the registry, validation, and lifecycle of registered callables.
- 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 asname,description,tags, etc.) are restricted from being overridden in this dictionary.input_schema¶meters: 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 toapplication/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 toTextResourceContents(retains text formatting).dict|list: Automatically serialized to JSON and mapped toTextResourceContents(defaults toapplication/jsonmime type).bytes: Base64 encoded and mapped toBlobResourceContents(binary format).- Other types: If the return type is not supported,
ResourceExecutionErroris 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$ returns30. - 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:
- MCP Initialization Handshake: Establishes protocol compatibility and negotiates capabilities.
- Tool Discovery (list_tools): Allows client to discover registered tools (
add,greet,failing_tool,get_info), mapping schemas, types, and descriptions accurately. - Tool Invocations (call_tool): Runs synchronous/asynchronous callables natively and returns outputs (structured data for dict results, and TextContent blocks for other primitive returns).
- 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 yourMCPServerinstance.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:
- MCPToolForge starts search from the current working directory (
Path.cwd()). - It climbs up parent directories looking for a
pyproject.tomlcontaining a[tool.mcptoolforge]section. - The directory containing
pyproject.tomlis 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
serveris missing, or if it is not an instance ofMCPServer, an error is raised. - If multiple
MCPServerinstances exist in the entrypoint file and none is namedserver, MCPToolForge raises an ambiguity error.
CLI Precedence & Overrides
MCPToolForge resolves the server to load using the following order of precedence:
- Explicit CLI arguments (e.g.,
mcptoolforge run --file custom_server.py) - Configuration values defined in
pyproject.toml - MCPToolForge defaults (
server.pyin 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 useprint()). Always write logs to standard error (stderr) using Python'sloggingmodule orsys.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_middlewarefor async pipelines,sync_timing_middlewarefor purely sync pipelines). - Logging: Traces tool parameters, entry, and exit statuses (
logging_middlewarefor async pipelines,sync_logging_middlewarefor 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
-
Create and activate a virtual environment:
python3 -m venv .venv source .venv/bin/activate
-
Install the package in editable mode:
pip install -e .
-
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c59174f3936f4d1f110c00ede371a1a0cd4c8615d8c52439098892ded89ef384
|
|
| MD5 |
c1ebb9e97e0f2ce9b455f8249b55f6bb
|
|
| BLAKE2b-256 |
05f8381088b8b664104421c02b88466e4df884b0d8480cd1e823a8afa5924419
|
Provenance
The following attestation bundles were made for mcptoolforge-0.1.0.tar.gz:
Publisher:
publish.yml on Lakshyalamba/toolforge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcptoolforge-0.1.0.tar.gz -
Subject digest:
c59174f3936f4d1f110c00ede371a1a0cd4c8615d8c52439098892ded89ef384 - Sigstore transparency entry: 2574792870
- Sigstore integration time:
-
Permalink:
Lakshyalamba/toolforge@77c8bc5ed7b26be5e9aaf3aa1632e4e715747351 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Lakshyalamba
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@77c8bc5ed7b26be5e9aaf3aa1632e4e715747351 -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b3e3583b40fd0481773ca08645eaedee9ba51e1cfacfc800d9dffce0ff592b4
|
|
| MD5 |
0b2ec2c0f804d10c9c35f4d89322ac9d
|
|
| BLAKE2b-256 |
985e378c9f8569172ac4d090849f6a56a43ec6ceaa2ef1ec61716309164d9067
|
Provenance
The following attestation bundles were made for mcptoolforge-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on Lakshyalamba/toolforge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcptoolforge-0.1.0-py3-none-any.whl -
Subject digest:
6b3e3583b40fd0481773ca08645eaedee9ba51e1cfacfc800d9dffce0ff592b4 - Sigstore transparency entry: 2574793166
- Sigstore integration time:
-
Permalink:
Lakshyalamba/toolforge@77c8bc5ed7b26be5e9aaf3aa1632e4e715747351 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Lakshyalamba
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@77c8bc5ed7b26be5e9aaf3aa1632e4e715747351 -
Trigger Event:
release
-
Statement type: