model-context-api 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- model_context_api-0.1.1/PKG-INFO +744 -0
- model_context_api-0.1.1/README.md +731 -0
- {model_context_api-0.1.0 → model_context_api-0.1.1}/mca/__init__.py +1 -1
- {model_context_api-0.1.0 → model_context_api-0.1.1}/mca/base.py +35 -17
- {model_context_api-0.1.0 → model_context_api-0.1.1}/mca/models.py +10 -2
- {model_context_api-0.1.0 → model_context_api-0.1.1}/mca/ninja.py +33 -11
- {model_context_api-0.1.0 → model_context_api-0.1.1}/pyproject.toml +2 -2
- model_context_api-0.1.0/PKG-INFO +0 -30
- model_context_api-0.1.0/README.md +0 -17
- {model_context_api-0.1.0 → model_context_api-0.1.1}/mca/mcp.py +0 -0
- {model_context_api-0.1.0 → model_context_api-0.1.1}/mca/pydantic.py +0 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: model-context-api
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Model Context API for AI Agents
|
|
5
|
+
Requires-Dist: pydantic>=2.13.5
|
|
6
|
+
Requires-Dist: mcp>=2.2,<3 ; extra == 'mcp'
|
|
7
|
+
Requires-Dist: django>=6.1.1 ; extra == 'ninja'
|
|
8
|
+
Requires-Dist: django-ninja>=1.7.0 ; extra == 'ninja'
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Provides-Extra: mcp
|
|
11
|
+
Provides-Extra: ninja
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Model Context API
|
|
15
|
+
|
|
16
|
+
model-context-api is a reusable registry and discovery layer for APIs that need
|
|
17
|
+
to be usable by both HTTP clients and AI agents.
|
|
18
|
+
|
|
19
|
+
The distribution name is model-context-api. The Python import package is mca:
|
|
20
|
+
|
|
21
|
+
~~~python
|
|
22
|
+
from mca.ninja import NinjaMCARouter
|
|
23
|
+
~~~
|
|
24
|
+
|
|
25
|
+
MCA provides:
|
|
26
|
+
|
|
27
|
+
- one operation registry shared by discovery, HTTP, and MCP;
|
|
28
|
+
- Markdown guides that can be discovered and read on demand;
|
|
29
|
+
- Pydantic dispatch for framework-independent engines;
|
|
30
|
+
- Django Ninja registration with generated request and response schemas;
|
|
31
|
+
- an MCP host that exposes registered Django applications as Streamable HTTP
|
|
32
|
+
tools.
|
|
33
|
+
|
|
34
|
+
## Why MCA exists
|
|
35
|
+
|
|
36
|
+
An ordinary API can be perfectly usable by a human developer and still be
|
|
37
|
+
difficult for an AI agent to use. The agent needs to answer several questions
|
|
38
|
+
before it can safely call an endpoint:
|
|
39
|
+
|
|
40
|
+
- What capabilities does this API provide?
|
|
41
|
+
- Which operation should handle the task?
|
|
42
|
+
- What does each input field mean?
|
|
43
|
+
- Which values belong in the URL, query string, or JSON body?
|
|
44
|
+
- What business rules or workflow constraints are not expressed by a type?
|
|
45
|
+
- What should the agent do after a successful or failed call?
|
|
46
|
+
|
|
47
|
+
MCA makes those answers available through one progressive contract. The
|
|
48
|
+
machine-readable part describes routes, parameters, request bodies, and
|
|
49
|
+
responses. The human-readable part explains concepts, policies, and workflows.
|
|
50
|
+
An agent can retrieve only the context it needs instead of receiving a large
|
|
51
|
+
undifferentiated prompt.
|
|
52
|
+
|
|
53
|
+
The normal interaction looks like this:
|
|
54
|
+
|
|
55
|
+
~~~text
|
|
56
|
+
agent
|
|
57
|
+
│
|
|
58
|
+
├─ GET / discover the API and list capabilities
|
|
59
|
+
├─ GET /?guide=... read the relevant domain instructions
|
|
60
|
+
├─ GET /?operation=... read one operation's request/response schema
|
|
61
|
+
└─ POST /items call the selected operation
|
|
62
|
+
│
|
|
63
|
+
└─ the same registry can also be reached through an MCP tool
|
|
64
|
+
~~~
|
|
65
|
+
|
|
66
|
+
MCA does not replace the application that owns the data or business logic. It
|
|
67
|
+
gives that application a consistent way to publish its capabilities and
|
|
68
|
+
context through HTTP, in-process dispatch, or MCP.
|
|
69
|
+
|
|
70
|
+
## Terminology
|
|
71
|
+
|
|
72
|
+
| Term | Meaning |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| Registry | The object that owns operations, routes, discovery, and guides for one API. |
|
|
75
|
+
| Operation | One callable capability, such as get_item or make_board. |
|
|
76
|
+
| Route | The HTTP method and path used to invoke an operation. |
|
|
77
|
+
| Schema | Machine-readable information describing valid inputs and outputs. |
|
|
78
|
+
| Guide | Markdown written by the API owner to explain concepts, rules, and workflows. |
|
|
79
|
+
| Discovery | The entry point that lists capabilities and tells clients how to request more context. |
|
|
80
|
+
| Adapter | The layer that connects the shared MCA registry to Pydantic or Django Ninja. |
|
|
81
|
+
| Transport | The way a client reaches the registry, such as direct HTTP or MCP. |
|
|
82
|
+
| MCP host | The ASGI bridge that exposes Django registries as MCP tools. |
|
|
83
|
+
|
|
84
|
+
The word registry refers to the application-facing object. For example,
|
|
85
|
+
mca_registry is a Django Ninja registry that can be discovered by MCPHost. The
|
|
86
|
+
word operation refers to a single capability inside that registry, not to the
|
|
87
|
+
registry itself.
|
|
88
|
+
|
|
89
|
+
## Installation
|
|
90
|
+
|
|
91
|
+
The base package includes Pydantic support:
|
|
92
|
+
|
|
93
|
+
~~~bash
|
|
94
|
+
python -m pip install model-context-api
|
|
95
|
+
~~~
|
|
96
|
+
|
|
97
|
+
Install the adapters that your application uses:
|
|
98
|
+
|
|
99
|
+
~~~bash
|
|
100
|
+
python -m pip install "model-context-api[ninja]"
|
|
101
|
+
python -m pip install "model-context-api[mcp]"
|
|
102
|
+
python -m pip install "model-context-api[ninja,mcp]"
|
|
103
|
+
~~~
|
|
104
|
+
|
|
105
|
+
The ninja extra installs Django and Django Ninja. The mcp extra installs the
|
|
106
|
+
MCP Python SDK. Applications using MCP normally install both extras.
|
|
107
|
+
|
|
108
|
+
The package requires Python 3.12 or newer.
|
|
109
|
+
|
|
110
|
+
## The MCA model
|
|
111
|
+
|
|
112
|
+
A registry is the API's published catalog. It connects each named operation
|
|
113
|
+
to its route, validation rules, response shape, description, and relevant
|
|
114
|
+
guides. The registry does not store application data and does not decide
|
|
115
|
+
whether a user is authenticated; the endpoint implementation and host
|
|
116
|
+
application still own those responsibilities.
|
|
117
|
+
|
|
118
|
+
Every registry includes a discovery operation named get_mca. With the default
|
|
119
|
+
mca_path="/", it is exposed as GET / relative to the API mount. Discovery is
|
|
120
|
+
deliberately separate from ordinary business operations so a client can learn
|
|
121
|
+
how to use an API before attempting a mutation.
|
|
122
|
+
|
|
123
|
+
Operation names determine their HTTP methods:
|
|
124
|
+
|
|
125
|
+
| Function prefix | HTTP method | Example operation |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| get_ | GET | get_item |
|
|
128
|
+
| make_ | POST | make_item |
|
|
129
|
+
| set_ | PUT | set_item |
|
|
130
|
+
| update_ | PATCH | update_item |
|
|
131
|
+
| remove_ | DELETE | remove_item |
|
|
132
|
+
|
|
133
|
+
The default operation name is the decorated function name. register_all can
|
|
134
|
+
generate several operations from one function by applying these prefixes.
|
|
135
|
+
|
|
136
|
+
### Choosing an adapter
|
|
137
|
+
|
|
138
|
+
Choose the smallest adapter that matches where the operation lives:
|
|
139
|
+
|
|
140
|
+
| Adapter | Use it when | What it adds |
|
|
141
|
+
| --- | --- | --- |
|
|
142
|
+
| PydanticMCARouter | The operation is a typed Python function or engine. | In-process validation, dispatch, and schemas. |
|
|
143
|
+
| NinjaMCARouter | The operation is part of a Django Ninja REST API. | HTTP route registration and OpenAPI-derived schemas. |
|
|
144
|
+
| MCPHost | MCP clients need access to Django registries. | MCP tools and Streamable HTTP endpoints. |
|
|
145
|
+
| BaseMCARouter | You are implementing another transport. | Shared registration, guides, resolution, and discovery behavior. |
|
|
146
|
+
|
|
147
|
+
The adapters share the same operation and discovery concepts. A route
|
|
148
|
+
registered with NinjaMCARouter can therefore be called directly over HTTP,
|
|
149
|
+
internally through execute_http_request, or through MCPHost without creating
|
|
150
|
+
three separate implementations.
|
|
151
|
+
|
|
152
|
+
## Guides and discovery
|
|
153
|
+
|
|
154
|
+
### What a guide is
|
|
155
|
+
|
|
156
|
+
A guide is a Markdown document written for the person or agent using an API.
|
|
157
|
+
It explains information that types and route schemas cannot fully express:
|
|
158
|
+
|
|
159
|
+
- what the resource represents;
|
|
160
|
+
- which workflow the operations belong to;
|
|
161
|
+
- business rules and state transitions;
|
|
162
|
+
- safety requirements before destructive actions;
|
|
163
|
+
- examples of valid combinations of operations;
|
|
164
|
+
- when to choose one operation or guide over another.
|
|
165
|
+
|
|
166
|
+
For example, a board API might use these guides:
|
|
167
|
+
|
|
168
|
+
~~~text
|
|
169
|
+
index.md
|
|
170
|
+
What the API is for and where an agent should begin.
|
|
171
|
+
api.md
|
|
172
|
+
Route conventions, request shapes, identifiers, and error rules.
|
|
173
|
+
workflow.md
|
|
174
|
+
Board states, card movement rules, terminal states, and history behavior.
|
|
175
|
+
~~~
|
|
176
|
+
|
|
177
|
+
The operation schema can say that a delete operation accepts a boolean
|
|
178
|
+
confirmed field. The guide can explain why deletion is destructive, when
|
|
179
|
+
confirmation is required, and what related history is removed. The schema is
|
|
180
|
+
for validation; the guide is for understanding and decision-making.
|
|
181
|
+
|
|
182
|
+
Guides are not executable code, are not a replacement for validation, and are
|
|
183
|
+
not automatically sent with every operation. Keeping them as separate
|
|
184
|
+
retrievable documents lets an agent first discover the API, then load only the
|
|
185
|
+
domain context needed for its current task.
|
|
186
|
+
|
|
187
|
+
### How discovery works
|
|
188
|
+
|
|
189
|
+
Discovery is the entry point for an unfamiliar client. A client should not
|
|
190
|
+
guess operation names or request shapes. It should progressively ask for:
|
|
191
|
+
|
|
192
|
+
1. the root document, which gives the API's purpose and available names;
|
|
193
|
+
2. the relevant guide, which gives domain and workflow context;
|
|
194
|
+
3. the selected operation schema, which gives exact input and output shape;
|
|
195
|
+
4. the operation route, which performs the requested work.
|
|
196
|
+
|
|
197
|
+
Guides are optional. Pass `guides_dir` to enable guide discovery and reading;
|
|
198
|
+
pass `guides_dir=None` (the default) for an API without guides. When guides are
|
|
199
|
+
disabled, guide-related fields are omitted from discovery responses and guide
|
|
200
|
+
requests return an `unknown_guides` error.
|
|
201
|
+
|
|
202
|
+
When guides are enabled, the guide directory should contain index.md:
|
|
203
|
+
|
|
204
|
+
~~~text
|
|
205
|
+
myapp/
|
|
206
|
+
├── api.py
|
|
207
|
+
└── guides/
|
|
208
|
+
├── index.md
|
|
209
|
+
├── items.md
|
|
210
|
+
└── workflows.md
|
|
211
|
+
~~~
|
|
212
|
+
|
|
213
|
+
With guides enabled, the root discovery response returns the registry metadata,
|
|
214
|
+
index content, available guide names, and a map of available operations. Guide
|
|
215
|
+
and operation details are requested separately. Without guides, the response
|
|
216
|
+
contains the registry metadata, help text, and available operations only:
|
|
217
|
+
|
|
218
|
+
~~~http
|
|
219
|
+
GET / Root discovery
|
|
220
|
+
GET /?guide=items.md Read one guide
|
|
221
|
+
GET /?guide=items.md,api.md Read several guides
|
|
222
|
+
GET /?operation=get_item Read one operation schema
|
|
223
|
+
~~~
|
|
224
|
+
|
|
225
|
+
A guide response maps each requested filename to its Markdown content.
|
|
226
|
+
Operations can advertise relevant guides without embedding those guides in
|
|
227
|
+
every response:
|
|
228
|
+
|
|
229
|
+
~~~python
|
|
230
|
+
@router.register("/items/{item_id}", guides=["items.md"])
|
|
231
|
+
def get_item(params: ItemParams) -> ItemOut:
|
|
232
|
+
...
|
|
233
|
+
~~~
|
|
234
|
+
|
|
235
|
+
Use include_in_discovery=False for a callable route that should not appear in
|
|
236
|
+
available_operations:
|
|
237
|
+
|
|
238
|
+
~~~python
|
|
239
|
+
@router.register("/internal/rebuild", include_in_discovery=False)
|
|
240
|
+
def make_rebuild() -> None:
|
|
241
|
+
...
|
|
242
|
+
~~~
|
|
243
|
+
|
|
244
|
+
### A complete agent session
|
|
245
|
+
|
|
246
|
+
Suppose an agent needs to create an item but has never seen this API. A
|
|
247
|
+
well-behaved client can follow this sequence:
|
|
248
|
+
|
|
249
|
+
1. Discover the API:
|
|
250
|
+
|
|
251
|
+
~~~http
|
|
252
|
+
GET /api/items/
|
|
253
|
+
~~~
|
|
254
|
+
|
|
255
|
+
The response says that the API manages items and lists make_item as a
|
|
256
|
+
creation operation.
|
|
257
|
+
|
|
258
|
+
2. Read the relevant domain guide:
|
|
259
|
+
|
|
260
|
+
~~~http
|
|
261
|
+
GET /api/items/?guide=items.md
|
|
262
|
+
~~~
|
|
263
|
+
|
|
264
|
+
The guide might explain naming rules, required relationships, or when an
|
|
265
|
+
item should be created instead of updated.
|
|
266
|
+
|
|
267
|
+
3. Read the exact operation schema:
|
|
268
|
+
|
|
269
|
+
~~~http
|
|
270
|
+
GET /api/items/?operation=make_item
|
|
271
|
+
~~~
|
|
272
|
+
|
|
273
|
+
The response identifies the POST route and describes the required body.
|
|
274
|
+
|
|
275
|
+
4. Invoke the operation:
|
|
276
|
+
|
|
277
|
+
~~~http
|
|
278
|
+
POST /api/items/items
|
|
279
|
+
Content-Type: application/json
|
|
280
|
+
|
|
281
|
+
{"name": "Example", "description": "Created after discovery."}
|
|
282
|
+
~~~
|
|
283
|
+
|
|
284
|
+
The same sequence can use an MCP tool instead: call items_api with
|
|
285
|
+
GET /, then request the guide and schema, then call items_api with POST /items
|
|
286
|
+
and the JSON body. The business operation is still the same registered
|
|
287
|
+
operation.
|
|
288
|
+
|
|
289
|
+
## Framework-independent Pydantic APIs
|
|
290
|
+
|
|
291
|
+
Use PydanticMCARouter when the engine should be callable without Django or
|
|
292
|
+
another web framework.
|
|
293
|
+
|
|
294
|
+
The example below publishes two capabilities: reading an item and creating an
|
|
295
|
+
item. The decorator supplies the route and optional description. If no
|
|
296
|
+
description is supplied, MCA uses the operation function's docstring. The
|
|
297
|
+
function name supplies the HTTP method and operation name. The Pydantic
|
|
298
|
+
annotations tell MCA which values are inputs and what a successful response
|
|
299
|
+
looks like.
|
|
300
|
+
|
|
301
|
+
~~~python
|
|
302
|
+
# myapp/engine.py
|
|
303
|
+
from pathlib import Path
|
|
304
|
+
|
|
305
|
+
from pydantic import BaseModel, Field
|
|
306
|
+
|
|
307
|
+
from mca.pydantic import PydanticMCARouter
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class ItemParams(BaseModel):
|
|
311
|
+
item_id: int = Field(..., description="Unique item identifier.")
|
|
312
|
+
verbose: bool = Field(False, description="Include the long description.")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class ItemCreate(BaseModel):
|
|
316
|
+
name: str = Field(..., min_length=1)
|
|
317
|
+
description: str = ""
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class ItemOut(BaseModel):
|
|
321
|
+
item_id: int
|
|
322
|
+
name: str
|
|
323
|
+
description: str
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
router = PydanticMCARouter(
|
|
327
|
+
guides_dir=Path(__file__).with_name("guides"),
|
|
328
|
+
title="Items API",
|
|
329
|
+
version=1.0,
|
|
330
|
+
help="Use operation and guide discovery before calling an item route.",
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@router.register(
|
|
335
|
+
"/items/{item_id}",
|
|
336
|
+
description="Read one item.",
|
|
337
|
+
guides=["items.md"],
|
|
338
|
+
)
|
|
339
|
+
def get_item(params: ItemParams) -> ItemOut:
|
|
340
|
+
return ItemOut(
|
|
341
|
+
item_id=params.item_id,
|
|
342
|
+
name=f"Item {params.item_id}",
|
|
343
|
+
description="Detailed." if params.verbose else "",
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@router.register("/items", description="Create an item.")
|
|
348
|
+
def make_item(data: ItemCreate) -> ItemOut:
|
|
349
|
+
return ItemOut(item_id=1, name=data.name, description=data.description)
|
|
350
|
+
~~~
|
|
351
|
+
|
|
352
|
+
There is no web server in this example. The registry is an in-process
|
|
353
|
+
dispatcher: a caller supplies an operation or route, MCA validates the input,
|
|
354
|
+
calls the Python function, and validates the result. A web or MCP adapter can
|
|
355
|
+
expose the same logical operations later.
|
|
356
|
+
|
|
357
|
+
The parameter conventions are:
|
|
358
|
+
|
|
359
|
+
- params is the typed path/query parameter object;
|
|
360
|
+
- data is the typed JSON body;
|
|
361
|
+
- the return annotation is the response schema;
|
|
362
|
+
- fields named in route placeholders such as {item_id} become path parameters;
|
|
363
|
+
- remaining params fields become query parameters.
|
|
364
|
+
|
|
365
|
+
A route such as /items/{item_id} with ItemParams produces this logical request
|
|
366
|
+
shape:
|
|
367
|
+
|
|
368
|
+
~~~json
|
|
369
|
+
{
|
|
370
|
+
"path_params": {"item_id": 7},
|
|
371
|
+
"query_params": {"verbose": true}
|
|
372
|
+
}
|
|
373
|
+
~~~
|
|
374
|
+
|
|
375
|
+
Dispatch by operation name or by an HTTP method and route path:
|
|
376
|
+
|
|
377
|
+
~~~python
|
|
378
|
+
# Root discovery.
|
|
379
|
+
discovery = router.dispatch("get_mca")
|
|
380
|
+
|
|
381
|
+
# Read guides and an operation schema.
|
|
382
|
+
guides = router.dispatch("get_mca", params={"guide": "items.md"})
|
|
383
|
+
schema = router.dispatch("get_mca", params={"operation": "get_item"})
|
|
384
|
+
|
|
385
|
+
# Dispatch a typed operation.
|
|
386
|
+
created = router.dispatch(
|
|
387
|
+
"make_item",
|
|
388
|
+
data={"name": "Example", "description": "Created by an agent."},
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Route paths resolve path placeholders and validate their values.
|
|
392
|
+
item = router.dispatch("/items/7", params={"verbose": True}, method="GET")
|
|
393
|
+
~~~
|
|
394
|
+
|
|
395
|
+
Inputs are validated before an endpoint is called, and results are validated
|
|
396
|
+
against return annotations. Invalid requests, unknown operations, unknown
|
|
397
|
+
routes, and endpoint failures are returned as ErrorOut values:
|
|
398
|
+
|
|
399
|
+
~~~python
|
|
400
|
+
result = router.dispatch("/items/not-an-integer", method="GET")
|
|
401
|
+
print(result.model_dump())
|
|
402
|
+
# {
|
|
403
|
+
# "code": "invalid_request",
|
|
404
|
+
# "detail": "...",
|
|
405
|
+
# "field": "item_id",
|
|
406
|
+
# }
|
|
407
|
+
~~~
|
|
408
|
+
|
|
409
|
+
Register several methods when one implementation has the same input and output
|
|
410
|
+
shape:
|
|
411
|
+
|
|
412
|
+
~~~python
|
|
413
|
+
@router.register_all(
|
|
414
|
+
"/items/{item_id}",
|
|
415
|
+
operation_id="item",
|
|
416
|
+
methods=("GET", "DELETE"),
|
|
417
|
+
)
|
|
418
|
+
def item(params: ItemParams) -> ItemOut | None:
|
|
419
|
+
if params.item_id == 0:
|
|
420
|
+
return None
|
|
421
|
+
return ItemOut(item_id=params.item_id, name="Example", description="")
|
|
422
|
+
~~~
|
|
423
|
+
|
|
424
|
+
This creates get_item and remove_item. Separate register decorators are clearer
|
|
425
|
+
when methods have different request or response models.
|
|
426
|
+
|
|
427
|
+
## Django Ninja APIs
|
|
428
|
+
|
|
429
|
+
Use NinjaMCARouter to register operations on either a NinjaAPI or a Django
|
|
430
|
+
Ninja Router. Options such as response, auth, tags, and other Django Ninja
|
|
431
|
+
route options are passed through to the corresponding registration method.
|
|
432
|
+
|
|
433
|
+
When a Router is supplied, its own auth and throttle configuration is used for
|
|
434
|
+
MCA schema discovery and internal execution. The Router can then be mounted on
|
|
435
|
+
a root NinjaAPI with `api.add_router(...)`; MCA does not require that root API
|
|
436
|
+
to generate operation schemas.
|
|
437
|
+
|
|
438
|
+
This adapter is for an existing HTTP API. It does not create a second business
|
|
439
|
+
logic layer: the decorated functions remain ordinary Django Ninja endpoints.
|
|
440
|
+
MCA adds the shared discovery operation, guide access, operation descriptions,
|
|
441
|
+
and a schema view assembled from Django Ninja's OpenAPI metadata. Router-backed
|
|
442
|
+
registries bind lazily to an internal NinjaAPI for this metadata and execution.
|
|
443
|
+
|
|
444
|
+
~~~python
|
|
445
|
+
# myapp/api.py
|
|
446
|
+
from pathlib import Path
|
|
447
|
+
|
|
448
|
+
from django.http import HttpRequest
|
|
449
|
+
from ninja import NinjaAPI, Path as NinjaPath, Query
|
|
450
|
+
from pydantic import BaseModel, Field
|
|
451
|
+
|
|
452
|
+
from mca.base import MCAError
|
|
453
|
+
from mca.ninja import NinjaMCARouter
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class ItemIn(BaseModel):
|
|
457
|
+
name: str = Field(..., min_length=1)
|
|
458
|
+
description: str = ""
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class ItemOut(BaseModel):
|
|
462
|
+
item_id: int
|
|
463
|
+
name: str
|
|
464
|
+
description: str
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
api = NinjaAPI(title="Items API", version="1.0")
|
|
468
|
+
|
|
469
|
+
# Name this mca_registry when MCPHost should discover it automatically.
|
|
470
|
+
mca_registry = NinjaMCARouter(
|
|
471
|
+
api,
|
|
472
|
+
guides_dir=Path(__file__).with_name("guides"),
|
|
473
|
+
title="Items API",
|
|
474
|
+
version=1.0,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
@mca_registry.register(
|
|
479
|
+
"/items",
|
|
480
|
+
response=list[ItemOut],
|
|
481
|
+
description="List items.",
|
|
482
|
+
)
|
|
483
|
+
def get_items(request: HttpRequest) -> list[ItemOut]:
|
|
484
|
+
return [ItemOut(item_id=1, name="Example", description="")]
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@mca_registry.register(
|
|
488
|
+
"/items/{item_id}",
|
|
489
|
+
response=ItemOut,
|
|
490
|
+
description="Read one item.",
|
|
491
|
+
)
|
|
492
|
+
def get_item(
|
|
493
|
+
request: HttpRequest,
|
|
494
|
+
item_id: int = NinjaPath(..., description="Unique item identifier."),
|
|
495
|
+
verbose: bool = Query(False, description="Include the long description."),
|
|
496
|
+
) -> ItemOut:
|
|
497
|
+
return ItemOut(
|
|
498
|
+
item_id=item_id,
|
|
499
|
+
name=f"Item {item_id}",
|
|
500
|
+
description="Detailed." if verbose else "",
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
@mca_registry.register(
|
|
505
|
+
"/items",
|
|
506
|
+
response=ItemOut,
|
|
507
|
+
description="Create an item.",
|
|
508
|
+
)
|
|
509
|
+
def make_item(request: HttpRequest, payload: ItemIn) -> ItemOut:
|
|
510
|
+
return ItemOut(item_id=2, name=payload.name, description=payload.description)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@api.exception_handler(MCAError)
|
|
514
|
+
def handle_mca_error(request: HttpRequest, exc: MCAError):
|
|
515
|
+
return api.create_response(
|
|
516
|
+
request,
|
|
517
|
+
{"code": exc.code, "detail": exc.detail, "field": exc.field},
|
|
518
|
+
status=exc.status,
|
|
519
|
+
)
|
|
520
|
+
~~~
|
|
521
|
+
|
|
522
|
+
Mount the NinjaAPI in Django as usual:
|
|
523
|
+
|
|
524
|
+
~~~python
|
|
525
|
+
# project/urls.py
|
|
526
|
+
from django.urls import path
|
|
527
|
+
|
|
528
|
+
from myapp.api import api
|
|
529
|
+
|
|
530
|
+
urlpatterns = [
|
|
531
|
+
path("api/items/", api.urls),
|
|
532
|
+
]
|
|
533
|
+
~~~
|
|
534
|
+
|
|
535
|
+
The resulting routes are:
|
|
536
|
+
|
|
537
|
+
~~~text
|
|
538
|
+
GET /api/items/ Root MCA discovery
|
|
539
|
+
GET /api/items/?guide=items.md Read a guide
|
|
540
|
+
GET /api/items/?operation=get_item Read an operation schema
|
|
541
|
+
GET /api/items/items/7 Read an item
|
|
542
|
+
POST /api/items/items Create an item
|
|
543
|
+
~~~
|
|
544
|
+
|
|
545
|
+
Routes registered with MCA are relative to the NinjaAPI mount. Both slash forms
|
|
546
|
+
are registered for non-root routes; the alternate form is hidden from the
|
|
547
|
+
generated schema and discovery output. Operation schemas are built from
|
|
548
|
+
Django Ninja's OpenAPI schema and describe path_params, query_params, and body.
|
|
549
|
+
|
|
550
|
+
This means a client can use the same API in two ways:
|
|
551
|
+
|
|
552
|
+
- a normal HTTP client follows the REST route and Django authentication rules;
|
|
553
|
+
- an agent first uses MCA discovery, reads the relevant context, and then calls
|
|
554
|
+
the same REST operation with a validated request.
|
|
555
|
+
|
|
556
|
+
### Internal Ninja dispatch
|
|
557
|
+
|
|
558
|
+
A registered operation can be executed without issuing an HTTP request or
|
|
559
|
+
re-entering ASGI. This is useful for internal orchestration and is also how
|
|
560
|
+
the MCP adapter invokes registered endpoints:
|
|
561
|
+
|
|
562
|
+
~~~python
|
|
563
|
+
response = mca_registry.execute_http_request(
|
|
564
|
+
"get_item",
|
|
565
|
+
source_request=request,
|
|
566
|
+
path_params={"item_id": 7},
|
|
567
|
+
query_params={"verbose": True},
|
|
568
|
+
)
|
|
569
|
+
~~~
|
|
570
|
+
|
|
571
|
+
execute_http_request creates a request with the registered method and route,
|
|
572
|
+
validates it through Django Ninja, and returns a Django HttpResponse. When
|
|
573
|
+
source_request is supplied, the authenticated user, cookies, session, and
|
|
574
|
+
relevant request metadata are copied.
|
|
575
|
+
|
|
576
|
+
For an already-created HttpRequest, use execute_http:
|
|
577
|
+
|
|
578
|
+
~~~python
|
|
579
|
+
response = mca_registry.execute_http(
|
|
580
|
+
"get_item",
|
|
581
|
+
request,
|
|
582
|
+
path_params={"item_id": 7},
|
|
583
|
+
)
|
|
584
|
+
~~~
|
|
585
|
+
|
|
586
|
+
Both methods are synchronous and reject asynchronous Ninja endpoints.
|
|
587
|
+
allow_anonymous=True marks a generated request as explicitly trusted internal
|
|
588
|
+
traffic. Only use it at a trusted boundary, and never expose it as a
|
|
589
|
+
user-controlled HTTP option.
|
|
590
|
+
|
|
591
|
+
## MCP hosting
|
|
592
|
+
|
|
593
|
+
MCP is a tool protocol used by clients that cannot or should not call an
|
|
594
|
+
application's REST API directly. An MCP tool gives the client a structured
|
|
595
|
+
entry point, while MCA keeps the tool contract aligned with the underlying
|
|
596
|
+
HTTP routes and schemas.
|
|
597
|
+
|
|
598
|
+
MCPHost wraps a Django ASGI application and automatically exposes every
|
|
599
|
+
installed Django app that publishes a mca_registry from its api module. Use
|
|
600
|
+
direct HTTP when the client can reach the REST API and should use its normal
|
|
601
|
+
authentication. Use MCP when the client needs a tool-oriented connection or
|
|
602
|
+
only has access to the MCP endpoint.
|
|
603
|
+
|
|
604
|
+
~~~python
|
|
605
|
+
# project/asgi.py
|
|
606
|
+
import os
|
|
607
|
+
|
|
608
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
|
|
609
|
+
|
|
610
|
+
from mca.mcp import MCPHost
|
|
611
|
+
|
|
612
|
+
application = MCPHost()
|
|
613
|
+
~~~
|
|
614
|
+
|
|
615
|
+
For an installed Django app whose label is items, the host provides:
|
|
616
|
+
|
|
617
|
+
~~~text
|
|
618
|
+
Streamable HTTP endpoint: /api/items/mcp
|
|
619
|
+
MCP tool name: items_api
|
|
620
|
+
REST API base path: /api/items
|
|
621
|
+
~~~
|
|
622
|
+
|
|
623
|
+
All other paths are passed to Django's normal ASGI application. The host
|
|
624
|
+
initializes Django, discovers registries, owns MCP session-manager lifespans,
|
|
625
|
+
and mounts stateless Streamable HTTP applications for each app.
|
|
626
|
+
|
|
627
|
+
Automatic discovery works by inspecting installed Django apps, importing each
|
|
628
|
+
app's api module, and looking for a NinjaMCARouter named mca_registry. The app
|
|
629
|
+
label determines both the endpoint path and the tool name. This convention is
|
|
630
|
+
why the registry must be defined in the app's api module with that exact name.
|
|
631
|
+
|
|
632
|
+
The tool accepts an API-relative HTTP-style route and an optional JSON body:
|
|
633
|
+
|
|
634
|
+
~~~text
|
|
635
|
+
items_api(route, body=None)
|
|
636
|
+
~~~
|
|
637
|
+
|
|
638
|
+
Start with discovery:
|
|
639
|
+
|
|
640
|
+
~~~text
|
|
641
|
+
items_api(route="GET /")
|
|
642
|
+
items_api(route="GET /?guide=items.md")
|
|
643
|
+
items_api(route="GET /?operation=get_item")
|
|
644
|
+
~~~
|
|
645
|
+
|
|
646
|
+
Then invoke operations relative to /api/items:
|
|
647
|
+
|
|
648
|
+
~~~text
|
|
649
|
+
items_api(route="GET /items/7")
|
|
650
|
+
items_api(
|
|
651
|
+
route="POST /items",
|
|
652
|
+
body={"name": "Created through MCP", "description": "Example"},
|
|
653
|
+
)
|
|
654
|
+
items_api(route="DELETE /items/7")
|
|
655
|
+
~~~
|
|
656
|
+
|
|
657
|
+
Do not include the REST or MCP prefix:
|
|
658
|
+
|
|
659
|
+
~~~text
|
|
660
|
+
Correct: GET /items/7
|
|
661
|
+
Incorrect: GET /api/items/items/7
|
|
662
|
+
Incorrect: GET /api/items/mcp/items/7
|
|
663
|
+
~~~
|
|
664
|
+
|
|
665
|
+
The route parser accepts a method and relative path with or without a leading
|
|
666
|
+
slash, parses query strings, preserves repeated query parameters, accepts JSON
|
|
667
|
+
bodies only for POST, PUT, and PATCH, and resolves path parameters against the
|
|
668
|
+
registered routes.
|
|
669
|
+
|
|
670
|
+
Successful results are returned as JSON text. A 204 response is represented as
|
|
671
|
+
null. Invalid routes, validation failures, unknown operations, and endpoint
|
|
672
|
+
errors are returned as MCP tool errors containing the MCA error code, detail,
|
|
673
|
+
field, and HTTP status.
|
|
674
|
+
|
|
675
|
+
### Building one MCP server manually
|
|
676
|
+
|
|
677
|
+
For applications that do not want automatic Django app discovery:
|
|
678
|
+
|
|
679
|
+
~~~python
|
|
680
|
+
from mca.mcp import MCPHost
|
|
681
|
+
from myapp.api import mca_registry
|
|
682
|
+
|
|
683
|
+
host = MCPHost()
|
|
684
|
+
server = host.build_server(mca_registry, "items")
|
|
685
|
+
application = server.streamable_http_app(
|
|
686
|
+
streamable_http_path="/",
|
|
687
|
+
stateless_http=True,
|
|
688
|
+
)
|
|
689
|
+
~~~
|
|
690
|
+
|
|
691
|
+
The resulting server exposes the items_api tool. The surrounding ASGI
|
|
692
|
+
application is responsible for starting the server's session manager and for
|
|
693
|
+
providing Django settings.
|
|
694
|
+
|
|
695
|
+
## Errors and authentication
|
|
696
|
+
|
|
697
|
+
An MCA error is a normal part of the published contract, not an implementation
|
|
698
|
+
detail. Clients need a stable code to decide whether to retry, ask for a
|
|
699
|
+
missing value, choose another operation, or report a domain failure.
|
|
700
|
+
|
|
701
|
+
Use MCAError for stable machine-readable errors:
|
|
702
|
+
|
|
703
|
+
~~~python
|
|
704
|
+
from mca.base import MCAError
|
|
705
|
+
|
|
706
|
+
raise MCAError(
|
|
707
|
+
"item_not_found",
|
|
708
|
+
"The requested item does not exist.",
|
|
709
|
+
field="item_id",
|
|
710
|
+
status=404,
|
|
711
|
+
)
|
|
712
|
+
~~~
|
|
713
|
+
|
|
714
|
+
The error has code, detail, an optional field, and an HTTP status. The Pydantic
|
|
715
|
+
adapter converts MCA errors and validation failures into ErrorOut values. The
|
|
716
|
+
Django Ninja adapter raises the error through the normal request path, so
|
|
717
|
+
register an application exception handler when the API needs a consistent JSON
|
|
718
|
+
shape. MCP converts the resulting HTTP error into an MCP tool error.
|
|
719
|
+
|
|
720
|
+
MCA does not impose an authentication policy. Normal Ninja requests use the
|
|
721
|
+
authentication configured on the NinjaAPI or route. Internal execution and MCP
|
|
722
|
+
hosting can explicitly opt into an application's trusted anonymous mode, but
|
|
723
|
+
that decision belongs to the application boundary.
|
|
724
|
+
|
|
725
|
+
## Public modules
|
|
726
|
+
|
|
727
|
+
~~~text
|
|
728
|
+
mca.base
|
|
729
|
+
BaseMCARouter, GuideCatalog, MCAError, RegisteredRoute
|
|
730
|
+
mca.models
|
|
731
|
+
ErrorOut, MCAResponseOut, APIRouteSchemaOut,
|
|
732
|
+
MCADiscoveryOut, DiscoveryParams
|
|
733
|
+
mca.pydantic
|
|
734
|
+
PydanticMCARouter
|
|
735
|
+
mca.ninja
|
|
736
|
+
NinjaMCARouter, MCAExecutionError
|
|
737
|
+
mca.mcp
|
|
738
|
+
MCPHost, MCPRoute
|
|
739
|
+
~~~
|
|
740
|
+
|
|
741
|
+
Use BaseMCARouter when implementing another transport adapter. A custom adapter
|
|
742
|
+
supplies discovery endpoints, transport registration, dispatch, and error
|
|
743
|
+
conversion while reusing route registration, method mapping, guide catalogs,
|
|
744
|
+
route resolution, and discovery behavior from the base class.
|