fastapi-webmcp
Expose selected FastAPI routes as tools on document.modelContext for browser
agents. The package generates tool schemas from FastAPI's OpenAPI document and
ships a small framework-free JavaScript bridge. It does not run an MCP server.
Use it at two levels:
- add
@webmcp_toolto ordinary FastAPI endpoints and use the packaged page; - load the framework-neutral runtime from an existing HTML, React, Vue, or other frontend and provide handlers for page-local client tools.
Install
uv add fastapi-webmcp
fastapi-webmcp requires FastAPI 0.141.1 or newer for app.frontend().
Quick start
from fastapi import FastAPI
from pydantic import BaseModel
from fastapi_webmcp import FastAPIWebMCP, webmcp_tool
app = FastAPI()
webmcp = FastAPIWebMCP(app)
class ItemCreate(BaseModel):
name: str
quantity: int
@app.get("/items", operation_id="list_items")
@webmcp_tool(read_only=True)
async def list_items():
return []
@app.post("/items", operation_id="create_item")
@webmcp_tool(description="Create an item.", untrusted_content=False)
async def create_item(item: ItemCreate):
return item
webmcp.mount(page="/agent")
Run the FastAPI application and open /agent. A supporting browser registers
list_items and create_item; other routes stay private.
The decorator belongs directly below the FastAPI route decorator. Explicit
operation_id values produce stable tool names.
A runnable version is included in
examples/basic.py:
uv run uvicorn examples.basic:app --reload
Tool kinds
The manifest supports three kinds of tools:
request: generated from a decorated FastAPI operation and executed as an HTTP request;static: returns fixed guidance without a request;client: dispatches to a handler explicitly supplied by the current page.
Static and client tools can join a default manifest:
from fastapi_webmcp import client_tool, static_tool
webmcp.add_tool(
static_tool(
name="room.guide",
description="Explain how to use this room.",
text="Read the document before editing it.",
)
)
webmcp.add_tool(
client_tool(
name="room.replace_content",
action="replace_content",
description="Replace the content in the current editor.",
input_schema={
"type": "object",
"properties": {"content": {"type": "string"}},
"required": ["content"],
"additionalProperties": False,
},
)
)
The manifest contains only the client action name. It never contains executable JavaScript.
Explicit header inputs
A decorator can expose an OpenAPI header parameter under a tool-friendly input name. Unmapped required headers remain an error.
@app.post("/documents/{slug}/comments")
@webmcp_tool(
name="room.comment",
headers={"agent_name": "X-Agent-Name"},
)
async def comment(
slug: str,
comment: CommentCreate,
agent_name: Annotated[str, Header(alias="X-Agent-Name")],
): ...
agent_name appears in the tool schema but is sent as X-Agent-Name, not in
the JSON body.
Dynamic page manifests
Use manifest_provider when the tool set depends on a document, user, or page
capability. A page-scoped path value can be bound so the agent cannot retarget
the tool to another document. The provider is resolved by FastAPI's dependency
system, so it can use Depends, Security, request validation, dependency
overrides, and yield dependencies in the same way as a path operation.
from typing import Annotated
from fastapi import Depends
from fastapi_webmcp import RequestTool, WebMCPManifest
async def page_manifest(
request: Request,
user: Annotated[User, Depends(current_user)],
) -> WebMCPManifest:
slug = request.query_params["slug"]
can_write = await user_can_write(user, slug)
tools = [tool.bind_path(slug=slug) for tool in webmcp.tools() if isinstance(tool, RequestTool)]
if can_write:
tools.append(replace_content_tool)
return WebMCPManifest(tools=tools, context={"slug": slug, "canWrite": can_write})
webmcp.mount(page="/_webmcp", manifest_provider=page_manifest)
The provider is evaluated for every manifest request. Applications should
still re-check write permission at every persistence boundary. Hiding a tool
from the manifest is a user-experience measure, not an authorization boundary.
A provider that accidentally returns None fails closed instead of falling
back to the default tool set.
Authentication and authorization
Use the same FastAPI dependencies for the manifest and the underlying API
operations. Security scopes and app.dependency_overrides are preserved:
from typing import Annotated
from fastapi import Security
async def page_manifest(
user: Annotated[
User,
Security(current_user, scopes=["documents:read"]),
],
) -> WebMCPManifest:
tools = readable_tools(user)
if user.has_permission("documents:write"):
tools.append(replace_content_tool)
return WebMCPManifest(tools=tools, context={"canWrite": user.can_write})
@app.patch("/documents/{slug}")
async def update_document(
slug: str,
update: DocumentUpdate,
user: Annotated[
User,
Security(current_user, scopes=["documents:write"]),
],
): ...
For a static manifest, or for checks whose return value is not needed by the provider, pass parameterless dependencies just as you would to a FastAPI path operation:
webmcp.mount(
page="/_webmcp",
dependencies=[Depends(current_user)],
)
These dependencies protect manifest.json. The packaged HTML and JavaScript
remain public because they contain no user data or credentials. Protect a
separate application frontend with its own FastAPI router dependencies when
needed.
Required Authorization header dependencies and cookie dependencies are
transport-managed and do not become agent inputs. Bearer headers must be
supplied with requestHeaders; cookies require credentials="same-origin".
The runtime always loads the same-origin manifest with browser credentials.
For cookie sessions, select credentials="same-origin" so request tools also
send the session cookie; enable CSRF protection for state-changing operations:
webmcp = FastAPIWebMCP(app, credentials="same-origin")
An existing frontend that uses bearer tokens can supply headers at request time without placing secrets in the manifest:
registerWebMCP({
manifestUrl: "/_webmcp/manifest.json",
requestHeaders: () => ({ Authorization: `Bearer ${readAccessToken()}` }),
})
requestHeaders is used only for same-origin manifest and tool requests. It
can be a headers object or an async callback receiving { kind, url, tool, signal }. The application-provided headers override agent-controlled header
inputs when names collide. Authentication headers such as Authorization and
Cookie cannot be exposed as agent-controlled @webmcp_tool inputs.
Do not authenticate the same request as different users through a session cookie and a bearer token. Applications that support both should reject ambiguous requests or define one unambiguous credential source.
Existing frontend integration
mount() serves runtime.js as an ES module. An existing frontend imports it
and supplies only the client actions it owns:
import { registerWebMCP } from "/_webmcp/runtime.js"
const registration = registerWebMCP({
manifestUrl: `/_webmcp/manifest.json?slug=${slug}`,
handlers: {
replace_content: async ({ content }, { signal, context }) => {
const previousContent = editor.getValue()
editor.replaceContent(content)
await saveDocument({ content, signal })
return { ok: true, previous_content: previousContent, context }
},
},
})
registration.ready.catch(console.error)
// Call registration.abort() from React useEffect cleanup, Vue onUnmounted,
// or a pagehide listener.
After login, logout, account switching, or a permission change, refresh the registration. The runtime fetches and authorizes the next manifest before it aborts the previous tool generation; a partially registered new generation is aborted as a group:
await registration.refresh()
Request tools are always authorized again by their FastAPI endpoints. Client tool handlers must also check current page authorization before changing UI state, and persistence endpoints must independently authorize every write. Tool and manifest fetches reject redirects so authentication credentials and request bodies cannot be redirected outside the generated application path.
The same function can be wrapped in a small React hook; the Python package has no React dependency.
A complete framework-free example is included in
examples/thinkroom_lite.py:
uv run uvicorn examples.thinkroom_lite:app --reload
# open http://127.0.0.1:8000/room?slug=demo&mode=edit
Adopt an existing application
Routes can be selected without decorators while prototyping:
webmcp = FastAPIWebMCP.from_fastapi(
app,
include_operations={"list_items", "create_item"},
)
webmcp.mount(page="/agent")
Selection by OpenAPI tag is also available through include_tags. Automatic
exposure of every OpenAPI route requires the explicit expose_all=True flag.
Request mapping
The generated browser bridge supports:
- path parameters;
- scalar and repeated query parameters;
- JSON object request bodies;
- JSON scalar and array request bodies;
- GET, POST, PUT, PATCH, and DELETE;
- cancellation through
AbortSignal; - same-origin URL enforcement;
- MCP-style text and error result envelopes.
Local OpenAPI $ref schemas are dereferenced before they reach the browser.
Required headers without an explicit headers={...} mapping, cookie
parameters, external references, cyclic schemas, multipart bodies, and form
bodies are rejected instead of being exposed incorrectly.
Security defaults
Only decorated or explicitly selected operations are exposed. Tool calls use
credentials: "omit" by default, so the browser's account cookies do not
silently authorize an agent. credentials="same-origin" is available as an
explicit opt-in, but applications are responsible for CSRF protection and
correct agent attribution.
The manifest is loaded with same-origin credentials so application-level dependencies can protect the page and manifest. It contains no credentials or authorization headers.
Development
uv sync
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run mypy
node --test tests/runtime.test.mjs
uv build
Release files for fastapi-webmcp 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fastapi_webmcp-0.3.0.tar.gz | 76.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fastapi_webmcp-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 96.3 kB
Release files / fastapi_webmcp-0.3.0.tar.gz
| Download URL | fastapi_webmcp-0.3.0.tar.gz |
|---|---|
| Size | 76.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
24dbbc567a1dbae718f26d5ab921ff870b23046593e172a0a03e29cda9be537d
|
|
BLAKE2b-256 checksum How to use checksums |
c1f4e5b31f95c00ccac4f40809f07d601df6e35efb1d7e70f24871769383fcc8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.
Transparency logRelease files / fastapi_webmcp-0.3.0-py3-none-any.whl
| Download URL | fastapi_webmcp-0.3.0-py3-none-any.whl |
|---|---|
| Size | 20.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
449cfd4fffeae08ef96694ac354b34e3cb4f48f26f60cd8cc5fb8b60295efaf5
|
|
BLAKE2b-256 checksum How to use checksums |
63c8cbad60eeb6ebd62e16383da457e4dbe68fc5f04cdf5c1448a806759d3054
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 2, 2026.
Transparency log