Skip to main content

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:

  1. the root document, which gives the API's purpose and available names;
  2. the relevant guide, which gives domain and workflow context;
  3. the selected operation schema, which gives exact input and output shape;
  4. 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:

  1. Discover the API:

    GET /api/items/
    

    The response says that the API manages items and lists make_item as a creation operation.

  2. Read the relevant domain guide:

    GET /api/items/?guide=items.md
    

    The guide might explain naming rules, required relationships, or when an item should be created instead of updated.

  3. Read the exact operation schema:

    GET /api/items/?operation=make_item
    

    The response identifies the POST route and describes the required body.

  4. 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)

Source distribution for model-context-api 0.1.1
File Size Uploaded Provenance
model_context_api-0.1.1.tar.gz 20.3 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for model-context-api 0.1.1
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full provenance

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