Model Context API
model-context-api is a reusable registry and discovery layer for APIs that need to be usable by both HTTP clients and AI agents.
The distribution name is model-context-api. The Python import package is mca:
from mca.ninja import NinjaMCARouter
MCA provides:
- one operation registry shared by discovery, HTTP, and MCP;
- Markdown guides that can be discovered and read on demand;
- Pydantic dispatch for framework-independent engines;
- Django Ninja registration with generated request and response schemas;
- an MCP host that exposes registered Django applications as Streamable HTTP tools.
Why MCA exists
An ordinary API can be perfectly usable by a human developer and still be difficult for an AI agent to use. The agent needs to answer several questions before it can safely call an endpoint:
- What capabilities does this API provide?
- Which operation should handle the task?
- What does each input field mean?
- Which values belong in the URL, query string, or JSON body?
- What business rules or workflow constraints are not expressed by a type?
- What should the agent do after a successful or failed call?
MCA makes those answers available through one progressive contract. The machine-readable part describes routes, parameters, request bodies, and responses. The human-readable part explains concepts, policies, and workflows. An agent can retrieve only the context it needs instead of receiving a large undifferentiated prompt.
The normal interaction looks like this:
agent
│
├─ GET / discover the API and list capabilities
├─ GET /?guide=... read the relevant domain instructions
├─ GET /?operation=... read one operation's request/response schema
└─ POST /items call the selected operation
│
└─ the same registry can also be reached through an MCP tool
MCA does not replace the application that owns the data or business logic. It gives that application a consistent way to publish its capabilities and context through HTTP, in-process dispatch, or MCP.
Terminology
| Term | Meaning |
|---|---|
| Registry | The object that owns operations, routes, discovery, and guides for one API. |
| Operation | One callable capability, such as get_item or make_board. |
| Route | The HTTP method and path used to invoke an operation. |
| Schema | Machine-readable information describing valid inputs and outputs. |
| Guide | Markdown written by the API owner to explain concepts, rules, and workflows. |
| Discovery | The entry point that lists capabilities and tells clients how to request more context. |
| Adapter | The layer that connects the shared MCA registry to Pydantic or Django Ninja. |
| Transport | The way a client reaches the registry, such as direct HTTP or MCP. |
| MCP host | The ASGI bridge that exposes Django registries as MCP tools. |
The word registry refers to the application-facing object. For example, mca_registry is a Django Ninja registry that can be discovered by MCPHost. The word operation refers to a single capability inside that registry, not to the registry itself.
Installation
The base package includes Pydantic support:
python -m pip install model-context-api
Install the adapters that your application uses:
python -m pip install "model-context-api[ninja]"
python -m pip install "model-context-api[mcp]"
python -m pip install "model-context-api[ninja,mcp]"
The ninja extra installs Django and Django Ninja. The mcp extra installs the MCP Python SDK. Applications using MCP normally install both extras.
The package requires Python 3.12 or newer.
The MCA model
A registry is the API's published catalog. It connects each named operation to its route, validation rules, response shape, description, and relevant guides. The registry does not store application data and does not decide whether a user is authenticated; the endpoint implementation and host application still own those responsibilities.
Every registry includes a discovery operation named get_mca. With the default mca_path="/", it is exposed as GET / relative to the API mount. Discovery is deliberately separate from ordinary business operations so a client can learn how to use an API before attempting a mutation.
Operation names determine their HTTP methods:
| Function prefix | HTTP method | Example operation |
|---|---|---|
| get_ | GET | get_item |
| make_ | POST | make_item |
| set_ | PUT | set_item |
| update_ | PATCH | update_item |
| remove_ | DELETE | remove_item |
The default operation name is the decorated function name. register_all can generate several operations from one function by applying these prefixes.
Choosing an adapter
Choose the smallest adapter that matches where the operation lives:
| Adapter | Use it when | What it adds |
|---|---|---|
| PydanticMCARouter | The operation is a typed Python function or engine. | In-process validation, dispatch, and schemas. |
| NinjaMCARouter | The operation is part of a Django Ninja REST API. | HTTP route registration and OpenAPI-derived schemas. |
| MCPHost | MCP clients need access to Django registries. | MCP tools and Streamable HTTP endpoints. |
| BaseMCARouter | You are implementing another transport. | Shared registration, guides, resolution, and discovery behavior. |
The adapters share the same operation and discovery concepts. A route registered with NinjaMCARouter can therefore be called directly over HTTP, internally through execute_http_request, or through MCPHost without creating three separate implementations.
Guides and discovery
What a guide is
A guide is a Markdown document written for the person or agent using an API. It explains information that types and route schemas cannot fully express:
- what the resource represents;
- which workflow the operations belong to;
- business rules and state transitions;
- safety requirements before destructive actions;
- examples of valid combinations of operations;
- when to choose one operation or guide over another.
For example, a board API might use these guides:
index.md
What the API is for and where an agent should begin.
api.md
Route conventions, request shapes, identifiers, and error rules.
workflow.md
Board states, card movement rules, terminal states, and history behavior.
The operation schema can say that a delete operation accepts a boolean confirmed field. The guide can explain why deletion is destructive, when confirmation is required, and what related history is removed. The schema is for validation; the guide is for understanding and decision-making.
Guides are not executable code, are not a replacement for validation, and are not automatically sent with every operation. Keeping them as separate retrievable documents lets an agent first discover the API, then load only the domain context needed for its current task.
How discovery works
Discovery is the entry point for an unfamiliar client. A client should not guess operation names or request shapes. It should progressively ask for:
- the root document, which gives the API's purpose and available names;
- the relevant guide, which gives domain and workflow context;
- the selected operation schema, which gives exact input and output shape;
- the operation route, which performs the requested work.
Guides are optional. Pass guides_dir to enable guide discovery and reading;
pass guides_dir=None (the default) for an API without guides. When guides are
disabled, guide-related fields are omitted from discovery responses and guide
requests return an unknown_guides error.
When guides are enabled, the guide directory should contain index.md:
myapp/
├── api.py
└── guides/
├── index.md
├── items.md
└── workflows.md
With guides enabled, the root discovery response returns the registry metadata, index content, available guide names, and a map of available operations. Guide and operation details are requested separately. Without guides, the response contains the registry metadata, help text, and available operations only:
GET / Root discovery
GET /?guide=items.md Read one guide
GET /?guide=items.md,api.md Read several guides
GET /?operation=get_item Read one operation schema
A guide response maps each requested filename to its Markdown content. Operations can advertise relevant guides without embedding those guides in every response:
@router.register("/items/{item_id}", guides=["items.md"])
def get_item(params: ItemParams) -> ItemOut:
...
Use include_in_discovery=False for a callable route that should not appear in available_operations:
@router.register("/internal/rebuild", include_in_discovery=False)
def make_rebuild() -> None:
...
A complete agent session
Suppose an agent needs to create an item but has never seen this API. A well-behaved client can follow this sequence:
-
Discover the API:
GET /api/items/The response says that the API manages items and lists make_item as a creation operation.
-
Read the relevant domain guide:
GET /api/items/?guide=items.mdThe guide might explain naming rules, required relationships, or when an item should be created instead of updated.
-
Read the exact operation schema:
GET /api/items/?operation=make_itemThe response identifies the POST route and describes the required body.
-
Invoke the operation:
POST /api/items/items Content-Type: application/json {"name": "Example", "description": "Created after discovery."}
The same sequence can use an MCP tool instead: call items_api with GET /, then request the guide and schema, then call items_api with POST /items and the JSON body. The business operation is still the same registered operation.
Framework-independent Pydantic APIs
Use PydanticMCARouter when the engine should be callable without Django or another web framework.
The example below publishes two capabilities: reading an item and creating an item. The decorator supplies the route and optional description. If no description is supplied, MCA uses the operation function's docstring. The function name supplies the HTTP method and operation name. The Pydantic annotations tell MCA which values are inputs and what a successful response looks like.
# myapp/engine.py
from pathlib import Path
from pydantic import BaseModel, Field
from mca.pydantic import PydanticMCARouter
class ItemParams(BaseModel):
item_id: int = Field(..., description="Unique item identifier.")
verbose: bool = Field(False, description="Include the long description.")
class ItemCreate(BaseModel):
name: str = Field(..., min_length=1)
description: str = ""
class ItemOut(BaseModel):
item_id: int
name: str
description: str
router = PydanticMCARouter(
guides_dir=Path(__file__).with_name("guides"),
title="Items API",
version=1.0,
help="Use operation and guide discovery before calling an item route.",
)
@router.register(
"/items/{item_id}",
description="Read one item.",
guides=["items.md"],
)
def get_item(params: ItemParams) -> ItemOut:
return ItemOut(
item_id=params.item_id,
name=f"Item {params.item_id}",
description="Detailed." if params.verbose else "",
)
@router.register("/items", description="Create an item.")
def make_item(data: ItemCreate) -> ItemOut:
return ItemOut(item_id=1, name=data.name, description=data.description)
There is no web server in this example. The registry is an in-process dispatcher: a caller supplies an operation or route, MCA validates the input, calls the Python function, and validates the result. A web or MCP adapter can expose the same logical operations later.
The parameter conventions are:
- params is the typed path/query parameter object;
- data is the typed JSON body;
- the return annotation is the response schema;
- fields named in route placeholders such as {item_id} become path parameters;
- remaining params fields become query parameters.
A route such as /items/{item_id} with ItemParams produces this logical request shape:
{
"path_params": {"item_id": 7},
"query_params": {"verbose": true}
}
Dispatch by operation name or by an HTTP method and route path:
# Root discovery.
discovery = router.dispatch("get_mca")
# Read guides and an operation schema.
guides = router.dispatch("get_mca", params={"guide": "items.md"})
schema = router.dispatch("get_mca", params={"operation": "get_item"})
# Dispatch a typed operation.
created = router.dispatch(
"make_item",
data={"name": "Example", "description": "Created by an agent."},
)
# Route paths resolve path placeholders and validate their values.
item = router.dispatch("/items/7", params={"verbose": True}, method="GET")
Inputs are validated before an endpoint is called, and results are validated against return annotations. Invalid requests, unknown operations, unknown routes, and endpoint failures are returned as ErrorOut values:
result = router.dispatch("/items/not-an-integer", method="GET")
print(result.model_dump())
# {
# "code": "invalid_request",
# "detail": "...",
# "field": "item_id",
# }
Register several methods when one implementation has the same input and output shape:
@router.register_all(
"/items/{item_id}",
operation_id="item",
methods=("GET", "DELETE"),
)
def item(params: ItemParams) -> ItemOut | None:
if params.item_id == 0:
return None
return ItemOut(item_id=params.item_id, name="Example", description="")
This creates get_item and remove_item. Separate register decorators are clearer when methods have different request or response models.
Django Ninja APIs
Use NinjaMCARouter to register operations on either a NinjaAPI or a Django Ninja Router. Options such as response, auth, tags, and other Django Ninja route options are passed through to the corresponding registration method.
When a Router is supplied, its own auth and throttle configuration is used for
MCA schema discovery and internal execution. The Router can then be mounted on
a root NinjaAPI with api.add_router(...); MCA does not require that root API
to generate operation schemas.
This adapter is for an existing HTTP API. It does not create a second business logic layer: the decorated functions remain ordinary Django Ninja endpoints. MCA adds the shared discovery operation, guide access, operation descriptions, and a schema view assembled from Django Ninja's OpenAPI metadata. Router-backed registries bind lazily to an internal NinjaAPI for this metadata and execution.
# myapp/api.py
from pathlib import Path
from django.http import HttpRequest
from ninja import NinjaAPI, Path as NinjaPath, Query
from pydantic import BaseModel, Field
from mca.base import MCAError
from mca.ninja import NinjaMCARouter
class ItemIn(BaseModel):
name: str = Field(..., min_length=1)
description: str = ""
class ItemOut(BaseModel):
item_id: int
name: str
description: str
api = NinjaAPI(title="Items API", version="1.0")
# Name this mca_registry when MCPHost should discover it automatically.
mca_registry = NinjaMCARouter(
api,
guides_dir=Path(__file__).with_name("guides"),
title="Items API",
version=1.0,
)
@mca_registry.register(
"/items",
response=list[ItemOut],
description="List items.",
)
def get_items(request: HttpRequest) -> list[ItemOut]:
return [ItemOut(item_id=1, name="Example", description="")]
@mca_registry.register(
"/items/{item_id}",
response=ItemOut,
description="Read one item.",
)
def get_item(
request: HttpRequest,
item_id: int = NinjaPath(..., description="Unique item identifier."),
verbose: bool = Query(False, description="Include the long description."),
) -> ItemOut:
return ItemOut(
item_id=item_id,
name=f"Item {item_id}",
description="Detailed." if verbose else "",
)
@mca_registry.register(
"/items",
response=ItemOut,
description="Create an item.",
)
def make_item(request: HttpRequest, payload: ItemIn) -> ItemOut:
return ItemOut(item_id=2, name=payload.name, description=payload.description)
@api.exception_handler(MCAError)
def handle_mca_error(request: HttpRequest, exc: MCAError):
return api.create_response(
request,
{"code": exc.code, "detail": exc.detail, "field": exc.field},
status=exc.status,
)
Mount the NinjaAPI in Django as usual:
# project/urls.py
from django.urls import path
from myapp.api import api
urlpatterns = [
path("api/items/", api.urls),
]
The resulting routes are:
GET /api/items/ Root MCA discovery
GET /api/items/?guide=items.md Read a guide
GET /api/items/?operation=get_item Read an operation schema
GET /api/items/items/7 Read an item
POST /api/items/items Create an item
Routes registered with MCA are relative to the NinjaAPI mount. Both slash forms are registered for non-root routes; the alternate form is hidden from the generated schema and discovery output. Operation schemas are built from Django Ninja's OpenAPI schema and describe path_params, query_params, and body.
This means a client can use the same API in two ways:
- a normal HTTP client follows the REST route and Django authentication rules;
- an agent first uses MCA discovery, reads the relevant context, and then calls the same REST operation with a validated request.
Internal Ninja dispatch
A registered operation can be executed without issuing an HTTP request or re-entering ASGI. This is useful for internal orchestration and is also how the MCP adapter invokes registered endpoints:
response = mca_registry.execute_http_request(
"get_item",
source_request=request,
path_params={"item_id": 7},
query_params={"verbose": True},
)
execute_http_request creates a request with the registered method and route, validates it through Django Ninja, and returns a Django HttpResponse. When source_request is supplied, the authenticated user, cookies, session, and relevant request metadata are copied.
For an already-created HttpRequest, use execute_http:
response = mca_registry.execute_http(
"get_item",
request,
path_params={"item_id": 7},
)
Both methods are synchronous and reject asynchronous Ninja endpoints. allow_anonymous=True marks a generated request as explicitly trusted internal traffic. Only use it at a trusted boundary, and never expose it as a user-controlled HTTP option.
MCP hosting
MCP is a tool protocol used by clients that cannot or should not call an application's REST API directly. An MCP tool gives the client a structured entry point, while MCA keeps the tool contract aligned with the underlying HTTP routes and schemas.
MCPHost wraps a Django ASGI application and automatically exposes every installed Django app that publishes a mca_registry from its api module. Use direct HTTP when the client can reach the REST API and should use its normal authentication. Use MCP when the client needs a tool-oriented connection or only has access to the MCP endpoint.
# project/asgi.py
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
from mca.mcp import MCPHost
application = MCPHost()
For an installed Django app whose label is items, the host provides:
Streamable HTTP endpoint: /api/items/mcp
MCP tool name: items_api
REST API base path: /api/items
All other paths are passed to Django's normal ASGI application. The host initializes Django, discovers registries, owns MCP session-manager lifespans, and mounts stateless Streamable HTTP applications for each app.
Automatic discovery works by inspecting installed Django apps, importing each app's api module, and looking for a NinjaMCARouter named mca_registry. The app label determines both the endpoint path and the tool name. This convention is why the registry must be defined in the app's api module with that exact name.
The tool accepts an API-relative HTTP-style route and an optional JSON body:
items_api(route, body=None)
Start with discovery:
items_api(route="GET /")
items_api(route="GET /?guide=items.md")
items_api(route="GET /?operation=get_item")
Then invoke operations relative to /api/items:
items_api(route="GET /items/7")
items_api(
route="POST /items",
body={"name": "Created through MCP", "description": "Example"},
)
items_api(route="DELETE /items/7")
Do not include the REST or MCP prefix:
Correct: GET /items/7
Incorrect: GET /api/items/items/7
Incorrect: GET /api/items/mcp/items/7
The route parser accepts a method and relative path with or without a leading slash, parses query strings, preserves repeated query parameters, accepts JSON bodies only for POST, PUT, and PATCH, and resolves path parameters against the registered routes.
Successful results are returned as JSON text. A 204 response is represented as null. Invalid routes, validation failures, unknown operations, and endpoint errors are returned as MCP tool errors containing the MCA error code, detail, field, and HTTP status.
Building one MCP server manually
For applications that do not want automatic Django app discovery:
from mca.mcp import MCPHost
from myapp.api import mca_registry
host = MCPHost()
server = host.build_server(mca_registry, "items")
application = server.streamable_http_app(
streamable_http_path="/",
stateless_http=True,
)
The resulting server exposes the items_api tool. The surrounding ASGI application is responsible for starting the server's session manager and for providing Django settings.
Errors and authentication
An MCA error is a normal part of the published contract, not an implementation detail. Clients need a stable code to decide whether to retry, ask for a missing value, choose another operation, or report a domain failure.
Use MCAError for stable machine-readable errors:
from mca.base import MCAError
raise MCAError(
"item_not_found",
"The requested item does not exist.",
field="item_id",
status=404,
)
The error has code, detail, an optional field, and an HTTP status. The Pydantic adapter converts MCA errors and validation failures into ErrorOut values. The Django Ninja adapter raises the error through the normal request path, so register an application exception handler when the API needs a consistent JSON shape. MCP converts the resulting HTTP error into an MCP tool error.
MCA does not impose an authentication policy. Normal Ninja requests use the authentication configured on the NinjaAPI or route. Internal execution and MCP hosting can explicitly opt into an application's trusted anonymous mode, but that decision belongs to the application boundary.
Public modules
mca.base
BaseMCARouter, GuideCatalog, MCAError, RegisteredRoute
mca.models
ErrorOut, MCAResponseOut, APIRouteSchemaOut,
MCADiscoveryOut, DiscoveryParams
mca.pydantic
PydanticMCARouter
mca.ninja
NinjaMCARouter, MCAExecutionError
mca.mcp
MCPHost, MCPRoute
Use BaseMCARouter when implementing another transport adapter. A custom adapter supplies discovery endpoints, transport registration, dispatch, and error conversion while reusing route registration, method mapping, guide catalogs, route resolution, and discovery behavior from the base class.
Metadata
Release files for model-context-api 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| model_context_api-0.1.1.tar.gz | 20.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| model_context_api-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 43.8 kB
Release files / model_context_api-0.1.1.tar.gz
| Download URL | model_context_api-0.1.1.tar.gz |
|---|---|
| Size | 20.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
35bf662e4f08201cff9f0f8f219ae1ae4d4542d14204a709aca921f9920df6f4
|
|
BLAKE2b-256 checksum How to use checksums |
ea1f4a316bdfcfbe00cde8679477ad6f9dda55b591fc6910a4a9ed0cc1587baf
|
| 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 24, 2026.
Transparency logRelease files / model_context_api-0.1.1-py3-none-any.whl
| Download URL | model_context_api-0.1.1-py3-none-any.whl |
|---|---|
| Size | 23.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9daa9e5532cc724a27c227fafe8194745bb24f7f5d30a5d4d68d92cc2b5fe335
|
|
BLAKE2b-256 checksum How to use checksums |
665f95b1ddf32d2c305d3cfc55ed9989b28bb7af292644d123a2f26fd92cad51
|
| 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 24, 2026.
Transparency logSecurity for model-context-api 0.1.1
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for model_context_api-0.1.1.tar.gz
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 24, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.