Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

falcon-mcp-server

PyPI - Version PyPI - Python Version

falcon-mcp-server is a lightweight library for building MCP server endpoints on top of your Falcon application.

Warning: falcon-mcp-server is a fast-moving project in its early stages. Caveat emptor!

Note: falcon-mcp-server was upgraded to the current version of the MCP standard with substantial help from LLMs, under human supervision.


Table of Contents

Installation

Install from PyPI:

pip install falcon-mcp-server[serve]

This is a fast-moving project in its early stages where new versions are not always immediately released to PyPI. To install directly from the main branch:

pip install git+https://github.com/falconry/falcon-mcp-server

Example Usage

See the example in examples/example.py, which exposes a tool, a prompt and a resource.

Change dir to examples/, and run with Uvicorn:

uvicorn --log-config logging.yaml --factory example:mcp.create_app

Connect with any AI agent or MCP inspector supporting the Streamable HTTP transport as of the 2026-07-28 revision of MCP. Earlier protocol revisions (the ones relying on the initialize handshake and sessions) are not supported.

For instance, assuming you have both fast-agent (pip install fast-agent-mcp) and Ollama installed on your system, you can run it against the previously started server (see above) as:

fast-agent go --url http://localhost:8000/mcp --model generic.granite4:3b --message "What is the current temperature in London?"

Omit --message to run interactively. Feel free to pull and use a different ollama model!

The server can introduce itself to clients under your own name, and provide instructions that clients may pass on to the LLM alongside the tools:

mcp = falcon_mcp_server.MCP(
    name='weather-example',
    title='Weather Example',
    version='1.2.3',
    instructions='Temperatures are only known for New York and London.',
)

A tool or prompt receives the context of the request via the parameter that it annotates with Context, if any. The context carries the Falcon request and, if you add the end point to your app on a templated route, the values of its fields:

import falcon.asgi

from falcon_mcp_server import Context


async def get_quota(context: Context) -> str:
    account = context.path_params['account']
    token = context.req.auth
    ...


mcp.add_tool(get_quota, annotations={'readOnlyHint': True})

app = falcon.asgi.App()
app.add_route('/{account:int}/mcp', mcp)

The annotations are hints describing the behavior of a tool to clients. The dictionary is typed as ToolAnnotations, so a type checker catches a misspelt or non-boolean hint.

The tools exposed to a request can be narrowed down with a filter. A tool that is filtered out is neither listed nor callable:

def tool_filter(context, tool):
    if context.req.get_param_as_bool('read_only'):
        return bool(tool.annotations.get('readOnlyHint'))
    return True


mcp = falcon_mcp_server.MCP(tool_filter=tool_filter)

A tool result that is not a string is rendered as JSON for the model. Pass a serializer to render it otherwise, e.g., as TOON:

# With toon-format, the official implementation:
mcp = falcon_mcp_server.MCP(result_serializer=toon_format.dumps)

# Or with toons, which is implemented in Rust:
mcp = falcon_mcp_server.MCP(result_serializer=toons.dumps)

As with tools, a parameter of the serializer annotated with Context receives the context of the request. This way, the format can also be chosen via the URL of the end point, such as /mcp?format=toon:

def serialize(result, context: Context) -> str:
    if context.req.get_param('format') == 'toon':
        return toon_format.dumps(result)
    return json.dumps(result)


mcp = falcon_mcp_server.MCP(result_serializer=serialize)

A tool argument can be mirrored into an HTTP header, e.g., for a reverse proxy to log or route on without parsing the request body. Annotate the argument with x-mcp-header in the input schema, as the example does for city, and clients send its value in the Mcp-Param-City header; a call where the header and the argument disagree is rejected:

mcp.add_tool(
    get_temperature,
    input_schema={
        'type': 'object',
        'properties': {
            'city': {'type': 'string', 'x-mcp-header': 'City'},
        },
        'required': ['city'],
    },
)

An exception raised by a tool is logged, and reported to the client as an internal error without any details. The exceptions are Falcon's HTTP errors, such as falcon.HTTPUnauthorized, which are rendered as usual. To tell the model what went wrong, so that it can correct itself, raise ToolError:

from falcon_mcp_server import ToolError


async def get_temperature(city: str) -> str:
    if city not in temperatures:
        raise ToolError(f'Unknown city; try one of: {", ".join(temperatures)}')
    ...

Besides the fixed contents of add_simple_resource, a resource can be read by a callable, which is awaited on every read. It returns text (str) or binary data (bytes), and receives the context in the same way as a tool does:

async def get_usage(context: Context) -> str:
    """Storage and bandwidth used by the account this month."""
    account = context.path_params['account']
    ...


mcp.add_resource('usage://current', 'application/json', get_usage)

A tool can be paired with an MCP App, i.e., an HTML widget that the supporting hosts render in place of the plain tool result. Serve the widget as a ui:// resource, and point the tool to it via meta. A dict that the tool returns is also sent as structured content, which is what the widget gets to render:

WIDGET_URI = 'ui://weather-example/temperature.html'

mcp.add_tool(show_temperature, meta={'ui': {'resourceUri': WIDGET_URI}})
mcp.add_resource(WIDGET_URI, 'text/html;profile=mcp-app', read_widget)

A widget that loads scripts, styles or media from elsewhere, or connects to other origins, declares them in the _meta of its resource; the host builds the Content Security Policy of the widget from them:

csp = {
    'resourceDomains': ['https://cdn.example.com'],
    'connectDomains': ['https://api.example.com'],
}
mcp.add_resource(
    WIDGET_URI,
    'text/html;profile=mcp-app',
    read_widget,
    meta={'ui': {'csp': csp}},
)

See examples/widget.py and examples/widget.html for the complete example. It runs in the same way as the first one:

uvicorn --log-config logging.yaml --factory widget:mcp.create_app

A host that runs in a web browser needs its origin to be allowed, see below.

Requests from web browsers (i.e., the ones carrying an Origin header) are rejected with HTTP 403 unless their origin is explicitly allowed. Optionally, the same origins can also be granted access via CORS:

mcp = falcon_mcp_server.MCP(allowed_origins=['http://localhost:6274'])
app = mcp.create_app(cors_enable=True)

To require authentication, pass an authenticator, i.e., an async callable that receives the context of a request, and tells whether the request is authenticated; any other request is rejected with HTTP 401. This is also where to check that an access token was issued for this very end point.

Clients can then sign the user in via OAuth if the end point describes itself with protected resource metadata (RFC 9728), which names its authorization server(s). The 401 response points to the metadata, and its resource is filled in from the request (honoring X-Forwarded-* headers behind a reverse proxy):

async def authenticate(context: Context) -> bool:
    token = context.req.auth
    ...


mcp = falcon_mcp_server.MCP(
    authenticator=authenticate,
    resource_metadata={
        'authorization_servers': ['https://auth.example.com'],
        'scopes_supported': ['mcp'],
    },
)

create_app() serves the metadata at /.well-known/oauth-protected-resource/mcp. On an app of your own, add the well-known route via the metadata suffix:

app.add_route('/{account:int}/mcp', mcp)
app.add_route(
    '/.well-known/oauth-protected-resource/{account:int}/mcp',
    mcp,
    suffix='metadata',
)

Instead of HTTP, the server can also serve a client that launches it as a subprocess, via the stdio transport. The example does so when run directly:

if __name__ == '__main__':
    mcp.run_stdio()

For instance, fast-agent can launch it itself (run from examples/, with the same Ollama model as above):

fast-agent go --stdio "python example.py" --model generic.granite4:3b --message "What is the current temperature in London?"

While serving, stdout is reserved for MCP messages, so anything that the app prints goes to stderr instead. The context of a request carries a placeholder Falcon request to localhost without any headers or parameters. The allowed origins and the resource metadata only apply to HTTP. So does the authenticator, which is why an end point configured with one refuses to serve stdio. Clients pass any credentials via environment variables instead, so check them before serving. To do that, or any other asynchronous setup, in the same event loop as the server, await serve_stdio() instead:

async def main():
    await log_in(os.environ['WEATHER_API_TOKEN'])
    await mcp.serve_stdio()


asyncio.run(main())

License

falcon-mcp-server is distributed under the terms of the Apache-2.0 license.

Metadata

Release files for falcon-mcp-server 0.2.0rc1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for falcon-mcp-server 0.2.0rc1
File Size Uploaded
falcon_mcp_server-0.2.0rc1.tar.gz 49.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for falcon-mcp-server 0.2.0rc1
File Interpreter ABI Platform
falcon_mcp_server-0.2.0rc1-py3-none-any.whl Python 3 none any Details

Total release size: 89.1 kB

Release files / falcon_mcp_server-0.2.0rc1.tar.gz

Download URL falcon_mcp_server-0.2.0rc1.tar.gz
Size 49.5 kB
Tags Source
SHA-256 checksum
How to use checksums
e65549ac8c4d514c8d6ea0bec512d6e60f9b1ef6a749857097ee938fdb8b8748
BLAKE2b-256 checksum
How to use checksums
f0ca2d789833277d31312e58a200b4cbbb4312b78f565ed6a7fc68b663612e49
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.8

Release files / falcon_mcp_server-0.2.0rc1-py3-none-any.whl

Download URL falcon_mcp_server-0.2.0rc1-py3-none-any.whl
Size 39.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6b1e28a311ff9cce1b4e0bfc2a658057f22229babd63fa81d6d85e88bce31ea9
BLAKE2b-256 checksum
How to use checksums
e9d0d622d452a90af859b0240ee6feb688948ca4bb7ab395ab3ebb909d36e9b5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.8

Release history Release notifications | RSS feed

This release

0.2.0rc1 This release

2 release files

0.1.0

2 release files

0.0.1

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page