Frappe MCP allows Frappe apps to function as MCP servers
Frappe MCP allows your Frappe Framework app to function as a Streamable HTTP MCP
server.
# app/app/mcp.py
import frappe_mcp
mcp = frappe_mcp.MCP("todo-mcp")
@mcp.tool()
def fetch_todos(username: str): ...
@mcp.tool()
def mark_done(name: str): ...
# MCP endpoint at: http://<BASE_URL>/api/method/app.mcp.handle_mcp
@mcp.register()
def handle_mcp(): ...
[!NOTE]
Why not use the official Python SDK?
The official Python SDK only supports async Python, i.e. it assumes that your
server is an ASGI server.Frappe Framework is not async, it makes use of Werkzeug, a WSGI server, and so
a from scratch implementation was needed.
[!IMPORTANT]
Frappe MCP is in a highly experimental state, there will be bugs, breaking
changes and large updates. Mostly without notice.
On GitHub, click the Index button on the top right to view the index.
Using PIP:
pip install frappe-mcp
Using UV:
uv add frappe-mcp
Frappe MCP is yet in its infancy, as of now it only supports Tools.
Remaining server features such as resources, prompts, tool streaming using SSE
will be added as needed.
If you are using a version of the Framework having the OAuth2 updates
(frappe#33188) then using Frappe
MCP with it should be pretty straight forward. You can view this video to check
out how to set up Auth on the MCP Inspector.
https://github.com/user-attachments/assets/a1783a36-7bea-4361-8c7b-bdbb9789877b
If your version does not contain these updates, you will have to register an
OAuth Client on your Framework instance for ht MCP client. You can check the
docs for this.
Frappe MCP is fairly straightforward to use. Most of the MCP specific heavy
lifting is handled for you.
To use frappe-mcp
you first create an instance of the mcp
object:
# app/app/mcp.py (same dir as hooks.py)
import frappe_mcp
mcp = frappe_mcp.MCP("your-app-mcp")
Each instance of an MCP
object can be used to register a single MCP endpoint.
You can create multiple of these objects if you need to serve multiple MCP
endpoints for instance to group functionality.
@mcp.tool
You use the instaniated object i.e. mcp
to register tools:
# app/app/tools/tools.py
from app.mcp import mcp
@mcp.tool()
def tool_name(a: int, b: str):
"""Description of what the tool does
Args:
a: Description for arg `a`.
b: Description for arg `b`.
"""
... # tool body
return value
[!TIP]
Using Google style docstrings and type annotations like in the example above
allows Frappe MCP to extract theinputSchema
for the tool without any additional
configuration.
If needed, you can manually provide the inputSchema
and other meta data like annotations.
Check the Tools section for more details.
@mcp.register
You use the instantiated object to mark a function as the entry point to your
MCP server, i.e. the function endpoint will be where your MCP server is served
from.
# app/app/mcp.py
@mcp.register()
def handle_mcp():
import app.tools.tools # ensures that your tools are registered
Once this is done, your MCP server should be serving at the REST endpoint for
the method (docs).
In this case the endpoint when running locally would be:
http://<SITE_NAME:PORT>/api/method/app.mcp.handle_mcp
[!WARNING]
The function body’s only purpose is to import files containing your tools.
If this is not done your tools will not be loaded as Frappe MCP does not know where
your tools are located.If your tools are in the same file, or have been imported globally, you can
leave the function body empty.
You can register tools, in the following ways:
@mcp.tool
decoratormcp.add_tool
method@mcp.tool
decoratorThe @mcp.tool
decorator registers a function as a tool that can be used by an LLM.
The decorator accepts the following optional arguments:
name
(optional str
): The name of the tool. If not provided, the function’s __name__
will be used.description
(optional str
): A description of what the tool does. If not provided, it will be extracted from the function’s docstring.input_schema
(optional dict
): The JSON schema for the tool’s input. If not provided, it will be inferred from the function’s signature and docstring.use_entire_docstring
(optional bool
): If True
, the entire docstring will be used as the tool’s description. Otherwise, only the first section is used (i.e. no Args
). Defaults to False
.annotations
(optional dict
): Additional context about the tool, such as validation information or examples of how to use it. This should be a dictionary conforming to the ToolAnnotations
TypedDict
structure.Example:
from frappe_mcp import ToolAnnotations, MCP
mcp = MCP()
annotations = ToolAnnotations(
title="Get Current Weather",
readOnlyHint=True,
)
@mcp.tool(annotations=annotations)
def get_current_weather(location: str, unit: str = "celsius"):
'''Get the current weather in a given location.'''
# ... implementation ...
mcp.add_tool
methodThe mcp.add_tool
method allows manually defining a tool, serving as an alternative to the @mcp.tool
decorator.
It takes a Tool
object as an arg.
Example:
from frappe_mcp import Tool, MCP
mcp = MCP()
def get_current_weather(location: str, unit: str = "celsius"):
'''Get the current weather in a given location.'''
# ... implementation ...
# Create a tool object
weather_tool = Tool(
name="get_current_weather",
description="...",
input_schema={'type':'object', 'properties':{ ... }},
output_schema=None,
annotations=None,
fn=get_current_weather,
)
# Add the tool to the MCP instance
mcp.add_tool(weather_tool)
The ToolAnnotations
can be used to provide additional tool annotations
defined by the MCP spec
(reference).
class ToolAnnotations(TypedDict, total=False):
title: str | None
readOnlyHint: bool | None
destructiveHint: bool | None
idempotentHint: bool | None
openWorldHint: bool | None
The Tool
object that is used when manually defining and registering a tool
using mcp.add_tool
.
class Tool(TypedDict):
name: str
description: str
input_schema: dict[str, Any]
output_schema: dict[str, Any] | None
annotations: ToolAnnotations | None
fn: Callable
Input schema refers to the JSON Schema definition that describes a tool’s parameters.
The following tool:
@mcp.tool()
def tool_name(a: int, b: str = "default"):
"""Description of what the tool does
Args:
a: Description for arg `a`.
b: Description for arg `b`.
"""
... # tool body
return value
will have this input schema:
{
"type": "object",
"properties": {
"a": {
"type": "integer",
"description": "Description for arg `a`."
},
"b": {
"type": "string",
"description": "Description for arg `b`."
}
},
"required": ["a"]
}
This input schema is generated from the tool body automatically when using the
decorator.
The MCP
class is the main class for creating an MCP server.
This class orchestrates the handling of JSON-RPC requests, manages a registry of
available tools, and integrates with a WSGI server (like Frappe Framework) to
expose MCP functionality.
In a Frappe application, you would typically create a single instance of this
class and use the @mcp.register()
decorator on an API endpoint. Tools can be
added using the @mcp.tool()
decorator.
For use in other Werkzeug-based servers, you can use the mcp.handle()
method
directly.
mcp.register
decoratorThis decorator is used in Frappe applications to designate a function as the
entry point for MCP requests. It wraps the function with the necessary logic to
handle JSON-RPC messages, including initializing the tool registry and routing
requests to the appropriate handlers.
The decorator accepts the following optional arguments:
allow_guest
(optional bool
): If True
, allows unauthenticated access to the endpoint. Defaults to False
.xss_safe
(optional bool
): If True
, response will not be sanitized for XSS. Defaults to False
.Example:
# In app/mcp.py
from frappe_mcp import MCP
mcp = MCP(name="my-mcp-server")
@mcp.register()
def handle_mcp():
'''The entry point for MCP requests.'''
# This function body is executed before request handling.
# It's a good place to import modules that register tools.
import app.tools
mcp.handle
methodThis method directly processes a werkzeug.Request
and returns a
werkzeug.Response
. It’s the core request handling logic.
This method can be used to integrate the MCP server into any Werkzeug-based application
i.e. even if you’re not using Frappe Framework, you can use this to handle MCP
endpoints in your server.
It accepts the following arguments:
request
: The werkzeug.Request
object containing the MCP request.response
: A werkzeug.Response
object to be populated with the MCP response.It returns the populated werkzeug.Response
object.
Frappe MCP comes with a handy CLI tool to help you verify that your MCP server is set up correctly.
Its check
command inspects your Frappe apps to ensure that frappe_mcp
is being used correctly. This is also the default command, so you can run it with frappe-mcp
or frappe-mcp check
.
It performs the following checks:
frappe_mcp
.Options:
--app
, -a
’: Check only a specific app.--verbose
, -v
: Show detailed information such as the input schema.Usage:
# After installing frappe-mcp and using it in your app
# In your frappe bench dir so that you can use the cli
source ./env/bin/activate
# Check all apps that might be using Frappe MCP
frappe-mcp
# Check specific app with verbose output
frappe-mcp check --app app_name --verbose
You can use the official
inspector tool to verify if
your MCP endpoints are being served correctly.
Make sure to:
frappe-mcp check
to get it).After this you’ll be prompted to login and authorize the client after which you
can use it to test out your MCP server.
[!NOTE]
You may skip the final step by setting the
allow_guests
flag, i.e:@mcp.register(allow_guests=True) def handle_mcp(): ...
This bypasses auth, so make sure you don’t do this in production.