Tool generation

How 0-mcp turns your existing resources into agent-callable tools โ€” names, schemas, custom routes, registry inspection.

CRUD verb mapping

For each resource, 0-mcp emits agent-callable tools that mirror the REST verbs. Tool names are verb-first, lowercase:

Resource verbRESTMCP tool
listGET /spaceslist_space
getGET /spaces/{id}get_space
createPOST /spacescreate_space
updatePATCH /spaces/{id}update_space
deleteDELETE /spaces/{id}delete_space

Tool name = <verb>_<slug(summary)> โ€” summary is slugified (whitespace and punctuation collapsed to _, lowercased). When summary is unset, falls back to a snake_case version of the class name (SpaceResource โ†’ space).

What drives the tool

class SpaceResource(BaseResource):
    summary = 'space'                       # โ†’ tool name suffix
    description = 'Workspaces.'             # โ†’ tool description

    create_schema = SpaceCreate             # โ†’ MCP inputSchema (POST)
    update_schema = SpaceUpdate             # โ†’ MCP inputSchema (PATCH)
    list_schema   = SpaceOut                # โ†’ MCP outputSchema

    filter_fields = ['active']              # โ†’ list_space input properties
    search_fields = ['name']                # โ†’ list_space.search
    order_fields  = ['created_at']          # โ†’ list_space.order_by

No second declaration. Every property the REST API uses, MCP uses too.

inputSchema

How 0-mcp builds it, in priority order:

  1. Pydantic schema present โ€” model.model_json_schema() is emitted as JSON Schema directly.
  2. No Pydantic schema โ€” Django field introspection: emits an object schema with the columns from create_fields / update_fields / list_fields, types mapped from Django field types (IntegerField โ†’ integer, EmailField โ†’ string with format=email, JSONField โ†’ object, etc.).
  3. Resource without model and no schema โ€” open object ({type: 'object', additionalProperties: true}). Dispatch handles it.

Example โ€” generated from Django introspection

{
  "name": "create_space",
  "description": "Workspaces.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "name":        {"type": "string", "maxLength": 100},
      "description": {"type": "string"}
    },
    "required": ["name"]
  }
}

Example โ€” generated from a Pydantic schema

class SpaceCreate(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    description: str = ''

class SpaceResource(BaseResource):
    create_schema = SpaceCreate

Becomes:

{
  "name": "create_space",
  "inputSchema": {
    "type": "object",
    "properties": {
      "name":        {"type": "string", "minLength": 1, "maxLength": 100},
      "description": {"type": "string", "default": ""}
    },
    "required": ["name"]
  }
}

outputSchema

Driven by list_schema when set. For list tools, wrapped in {meta, objects}. For detail/create/update, the schema is used directly. When neither list_schema nor edit_fields is set, falls back to model introspection.

Custom routes

Custom routes decorated with @openapi(...) are exposed automatically:

from zeromcp import openapi
from pydantic import BaseModel

class SearchInput(BaseModel):
    query: str
    limit: int = 10

class SearchOutput(BaseModel):
    results: list[dict]

class UserResource(BaseResource):
    summary = 'user'
    routes = [{'path': r'/search$', 'func': 'search', 'allowed_methods': ['post']}]

    @openapi(
        summary='Semantic user search',
        description='Hybrid keyword + vector search. Returns ranked results.',
        request=SearchInput,
        response=SearchOutput,
    )
    async def search(self, request, match=None, body=None):
        ...

โ†’ Tool search_user with description, input and output schemas straight from the decorator.

Registry inspection โ€” built-in routes

When MCP is enabled (get_routes(endpoints, mcp=True)), two inspection routes are registered alongside the JSON-RPC endpoint:

RouteReturns
GET /mcp/tools.jsonMachine-readable list of tool definitions (the same as the agent sees via tools/list)
GET /mcp/toolsHuman-readable HTML page โ€” name, description, collapsible input/output schemas

Both honour the same auth as /docs: session cookie, X-Api-Key, or Authorization: Bearer when configured. REQUIRE_VALID_BEARER strict mode applies here too. Pass docs_public=True to get_routes for anonymous access.

Programmatic access

from zeromcp.mcp import list_tools_public

for tool in list_tools_public(endpoints):
    print(tool['name'], 'โ€”', tool['description'])

Returns the same definitions the agent receives via tools/list, minus the internal metadata that drives dispatch.

When you need the internal metadata (writing tests, custom dispatchers):

from zeromcp.mcp import list_tools

tools = list_tools(endpoints)
# [{'name': 'list_space', 'inputSchema': ..., 'mcp_internal': {...}}, ...]
๐Ÿชž

The registry is derived, not maintained. Add a field to a Pydantic schema, the tool's inputSchema updates. Add a custom route with @openapi, a new tool appears. Nothing to register, nothing to wire up.

0-mcp by Stamatios Stamou Jr โ€” github.com/ssjunior/0-mcp