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