Metadata-Version: 2.5
Name: webmcp-django
Version: 0.2.1
Summary: Server-side WebMCP toolkit for Django: tool registry, script-safe manifests, shared browser runtime, CSRF and Origin-Trial template tags.
Project-URL: Homepage, https://github.com/seunghan91/webmcp-django
Project-URL: Repository, https://github.com/seunghan91/webmcp-django
Project-URL: Changelog, https://github.com/seunghan91/webmcp-django/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/seunghan91/webmcp-django/issues
Author-email: seunghan Kim <theqwe2000@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: ai-agents,django,mcp,origin-trial,webmcp
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: django<6,>=4.2
Provides-Extra: test
Requires-Dist: pytest>=7; extra == 'test'
Description-Content-Type: text/markdown

# webmcp-django

[![PyPI](https://img.shields.io/pypi/v/webmcp-django)](https://pypi.org/project/webmcp-django/) [![Python](https://img.shields.io/pypi/pyversions/webmcp-django)](https://pypi.org/project/webmcp-django/) [![CI](https://github.com/seunghan91/webmcp-django/actions/workflows/ci.yml/badge.svg)](https://github.com/seunghan91/webmcp-django/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

Server-side WebMCP toolkit for Django — Django is the only dependency.

[WebMCP](https://github.com/webmachinelearning/webmcp) is a W3C Community Group
proposal that lets a web page register tools an in-browser AI agent can call
through `document.modelContext`. This package is the server side of that: you define
tools where your app already knows its routes, sessions and permissions, and a
small browser runtime registers them on the pages you choose. When an agent calls
a tool, the runtime calls your existing same-origin endpoint with the user's
session and CSRF token, so authentication and authorization stay in your app.

- **Tool definitions** — `webmcp_django.tools.Tool` and `register`, validated against the spec's naming, annotation and schema rules.
- **Template tags** — `{% webmcp_manifest "list_tasks" %}` (per-page opt-in), `{% webmcp_runtime %}` (CSP nonce aware, autostart), `{% webmcp_csrf_meta %}` (works with `CSRF_COOKIE_HTTPONLY`).
- **Declarative forms** — `{% webmcp_tool %}` and `{% webmcp_param %}` with unconditional escaping.
- **Origin Trial** — `OriginTrialMiddleware` and `{% webmcp_origin_trial_meta %}`.
- **Shared browser runtime** — shipped as a static file; same-origin only, CSRF read per call, declared parameters only, no retries.

```sh
pip install webmcp-django
```

**Status:** 0.x, tracking the WebMCP Draft CG Report of 2026-10-02. WebMCP runs
behind a Chrome origin trial (Chrome 149–156, extension requested to 162) or the
`chrome://flags/#enable-webmcp-testing` flag. The shared runtime is tested in real
Chrome 154 by the [Ruby reference suite](https://github.com/seunghan91/webmcp/blob/main/test/integration/RESULTS.md)
(CSRF-protected writes, blocked redirects, HTTP errors, Turbo navigation, strict CSP).

| Language | Package | Registry |
|---|---|---|
| Ruby / Rails (reference) | [`webmcp`](https://github.com/seunghan91/webmcp) | [RubyGems](https://rubygems.org/gems/webmcp) |
| Go (`net/http`) | [`webmcp-go`](https://github.com/seunghan91/webmcp-go) | [pkg.go.dev](https://pkg.go.dev/github.com/seunghan91/webmcp-go) |
| Python / Django | [`webmcp-django`](https://github.com/seunghan91/webmcp-django) | [PyPI](https://pypi.org/project/webmcp-django/) |
| Rust | [`webmcp`](https://github.com/seunghan91/webmcp-rust) | [crates.io](https://crates.io/crates/webmcp) |

All four emit the same manifest v1 (checked against shared conformance fixtures,
fingerprints included) and ship the byte-identical browser runtime.

## Intent: share identity, project the rest explicitly

**Surfaces are intentionally different; share identity, project the rest explicitly.**
A server MCP tool and its browser counterpart can represent the same feature
while intentionally having different schemas, limits, and execution paths.

| Difference | Server MCP | Browser WebMCP | Reason |
|---|---|---|---|
| Field names | `content`, `id` | `title`, `task_id` | Match visible labels for browser agents. |
| Dates | ISO 8601 | `YYYY-MM-DD HH:MM` in the user's timezone | Match what the user sees. |
| Result limit | 500 | 20 | Fit the browser response budget and tab context. |
| Execution path | Service object → database | Session cookies → existing web endpoint | Preserve session, CSRF and authorization checks. |

Share identity and reviewed descriptions; declare schema, limits, field mappings,
annotations and endpoints explicitly for each surface. MCP and WebMCP annotations
are different sets: `destructiveHint` is not automatically `consequentialHint`.
Even `read_only` must describe the browser endpoint itself. The Ruby reference
provides `from_mcp`-style projection; a Django SDK bridge is planned, not included.

## Quick start

Python >= 3.10; Django 4.2 LTS or 5.x. Django is the only runtime dependency.

```sh
pip install 'webmcp-django>=0.2,<0.3'
```

```python
# settings.py — retain your existing apps and middleware, including CSRF protection.
INSTALLED_APPS = [
    # ...
    "django.contrib.staticfiles",
    "webmcp_django",
]
MIDDLEWARE = [
    # ...
    "django.middleware.csrf.CsrfViewMiddleware",
    "webmcp_django.middleware.OriginTrialMiddleware",
]
STATIC_URL = "/static/"
WEBMCP_ORIGIN_TRIAL_TOKEN = "YOUR_ORIGIN_TRIAL_TOKEN"
WEBMCP_WARN_ON_OAC_OPT_OUT = True  # Optional, logs once per process.
```

Define and register tools at boot (for example, import your tool module from
`AppConfig.ready()`):

```python
from webmcp_django.tools import Tool, register, freeze_registry

register(Tool(
    name="list_tasks",
    title="List tasks",
    description="List at most 20 tasks for the signed-in user.",
    input_schema={
        "type": "object",
        "properties": {
            "tags": {"type": "array", "items": {"type": "string"}},
            "completed": {"type": "boolean"},
        },
    },
    annotations={"read_only": True, "untrusted_content": True},
    endpoint={"path": "/api/tasks", "method": "GET"},
    max_response_chars=1500,
))
register(Tool(
    name="create_task",
    description="Create one task.",
    input_schema={
        "type": "object",
        "properties": {"title": {"type": "string", "maxLength": 120}},
        "required": ["title"],
    },
    annotations={"consequential": True},
    endpoint={"path": "/api/tasks", "method": "POST", "param_map": {"title": "content"}},
))
# Call once after ALL application tool modules have been imported.
freeze_registry()
```

Definitions are copied and deeply frozen; invalid metadata and duplicate names
raise `DefinitionError` (a `ValueError`). Registration does not expose tools.
Only explicitly listed tools appear on a page; no names means no tools.

```django
{% load webmcp %}
{% webmcp_csrf_meta %}
{% webmcp_manifest "list_tasks" "create_task" %}
{% webmcp_runtime %}
```

Render with a request, e.g. `render(request, "tasks.html", context)`. The CSRF
helper obtains a masked token from that request and works with
`CSRF_COOKIE_HTTPONLY=True`; keep `CsrfViewMiddleware` enabled. The default
transport is `{"csrf":{"source":"meta","name":"csrf-token","header":"X-CSRFToken"}}`.
Writes use JSON bodies, so endpoints should parse `request.body` as JSON rather
than expecting Django's form-only `request.POST`.

The manifest has `data-webmcp-autostart` by default. The external module starts
it without inline executable JavaScript. The runtime helper resolves
`static('webmcp_django/webmcp-runtime.js')`; deploy it with your normal static
files/`collectstatic` setup. Both script helpers include an escaped
`request.csp_nonce` when present. For manual ownership, render
`{% webmcp_manifest "list_tasks" autostart=False %}` and import from your
application's external JavaScript entry:

```javascript
import { mount } from "/static/webmcp_django/webmcp-runtime.js";
const handle = mount({ selector: "#webmcp-manifest" });
// After replacing the manifest in an SPA:
handle.refresh();
// When its owner is removed:
handle.dispose();
```

Adapt the import URL to your static storage. Turbo refresh is built into the
runtime; other SPAs must call `refresh()` explicitly. Autostart exposes its handle
as `globalThis.WebMCPRuntime.handle`. Unsupported browsers are a no-op.

### Existing declarative tags and Origin Trial

```django
{% load webmcp %}
{% webmcp_origin_trial_meta %}
<form method="post" {% webmcp_tool "create_task" "Create a new task" autosubmit=True %}>
  {% csrf_token %}
  <input name="title" {% webmcp_param "The task title" %}>
</form>
```

Valid existing usage keeps the same output. Tool names must now satisfy
1..128 ASCII letters, digits, `_`, `.` or `-`. Only `toolname`, `tooldescription`,
`toolautosubmit` (boolean attribute), and `toolparamdescription` are emitted.
Values are always HTML-escaped, including `mark_safe` values.

The middleware fills only a missing `Origin-Trial` header, preserving even an
existing empty header. An empty configured token is a pass-through. With a token
configured, `WEBMCP_WARN_ON_OAC_OPT_OUT=True` logs once per process for
`Origin-Agent-Cluster: ?0`; the middleware never rewrites OAC or forces `?1`.
The meta helper is an alternative token delivery mechanism and emits nothing
when no token is configured.

### Building manifests directly

```python
from webmcp_django.manifest import build, to_script_tag
from webmcp_django.tools import get_tool

manifest = build([get_tool("list_tasks")])
html = to_script_tag(manifest, autostart=False, nonce="YOUR_CSP_NONCE")
# Override transport explicitly if your application uses another CSRF source:
manifest = build([get_tool("create_task")], transport={
    "csrf": {"source": "cookie", "name": "csrftoken", "header": "X-CSRFToken"},
})
```

Cookie transport requires a JavaScript-readable cookie. `transport={}` explicitly
omits CSRF transport; it is intended for read-only integrations. Template calls
can pass `transport=transport_config`. Empty `paramMap`, absent `title`, and absent
`maxResponseChars` are omitted; only true annotations are emitted. Fingerprints
include the effective tool entry and transport, using recursively sorted compact
UTF-8 JSON without HTML escaping. Script embedding applies escaping separately.

## Security model


The server remains the security boundary. Tools call existing same-origin
endpoints with the current session; those endpoints must enforce authorization,
CSRF, input validation, range limits and result caps. A schema `maximum` is
metadata, not server enforcement. Annotations are hints, not security controls.

- Endpoint paths must begin with `/`; protocol-relative URLs, backslashes, colons
  and control characters are rejected. The runtime also checks the resolved
  origin and uses same-origin mode/credentials with redirects rejected.
- GET requires `read_only: true`; read-only POST is allowed. Other methods need a
  CSRF token read at invocation time. Missing tokens stop the request.
- Only declared input keys are sent. Prototype-related keys are rejected.
  Explicit `param_map` destinations cannot collide or target reserved transport
  fields such as `_method`, `authenticity_token`, or `csrfmiddlewaretoken`.
- JSON embedding escapes `<`, `>`, `&`, U+2028 and U+2029. This prevents script
  breakout, including mixed-case closing tags and HTML comments. Nonces and HTML
  attributes are independently escaped.
- The runtime returns structured success/error envelopes and never retries.
  An ambiguous write result is `unknown_outcome`: verify with the user before
  retrying. A successful write with unreadable/oversized output stays successful
  with `dataOmitted`; an oversized read returns `response_too_large`. Responses
  are never silently truncated.

**Tool metadata is agent-visible, not just display text.** An AI agent reads
`tooldescription` / `toolparamdescription` as part of its instructions for what
the tool does. Do not build these strings from unvalidated user input (profile
fields, query params, uploaded file names, etc.); a user-controlled value rendered
into tool metadata is a prompt-injection vector that can hijack the agent's
behavior. Keep tool names and descriptions as literal strings you write, not
values derived at request time from data a visitor controls. Define tools at boot
and freeze the registry; HTML escaping alone does not prevent prompt injection.


## Conformance

[Fixtures](conformance/fixtures) are copied verbatim from the
[Ruby reference](https://github.com/seunghan91/webmcp). Tests compare parsed
manifest entries, including fixed fingerprints, without regenerating expectations.
See [the contract](conformance/README.md). The shared runtime is copied unchanged
and checked against [RUNTIME.sha256](conformance/RUNTIME.sha256).

```sh
python3 -m venv .venv
.venv/bin/pip install -e '.[test]' build
.venv/bin/pytest -q
.venv/bin/python -m build
```

The suite covers definition validation, page opt-in, script/attribute escaping,
Django `QueryDict.getlist` array round trips, and real `CsrfViewMiddleware`
rejection/success through `Client(enforce_csrf_checks=True)` with HttpOnly cookies.
It does not claim live Chrome or production CSP validation.

## Limitations

This 0.x package implements a deliberate schema subset. Root schemas accept
`type: "object"`, `properties`, `required`, and `description`. Properties accept
`string`, `number`, `integer`, `boolean`, or arrays of scalars, with `enum`,
`description`, `default`, `minimum`, `maximum`, `maxLength`, and `maxItems` metadata.
Nested objects/arrays, `$ref`, composition and unknown keywords are rejected.
Metadata numbers must be integers within +/-2^53; floats are rejected. Runtime
`number` inputs may be fractional. The endpoint enforces bounds, lengths and enums.

GET requires `read_only=True`; read-only POST is allowed. GET array properties
emit `arrayFormat: "repeat"` by default (`tags=a&tags=b`, read using
`request.GET.getlist("tags")`). Set `endpoint["array_format"]` to `"brackets"`
only for endpoints that expect bracketed keys. The shared Ruby array fixture
explicitly selects brackets and therefore has the same fingerprint here.

No cross-origin exposure, MCP SDK bridge, automatic response truncation, or
Inertia adapter is included. The draft and browser implementation can change.

Sibling packages: [Ruby](https://github.com/seunghan91/webmcp),
[Go](https://github.com/seunghan91/webmcp-go),
[Django](https://github.com/seunghan91/webmcp-django),
[Rust](https://github.com/seunghan91/webmcp-rust).

## License

MIT
