MCP Protocol Base
The Model Context Protocol, over Streamable HTTP, in Django: what every MCP server of the app shares.
MCPServerBase answers the JSON-RPC messages of one POST request, and MCPToolBase
is the base class of a tool. A server names itself, describes itself, gives its instructions, and
holds its tools. Everything else, the initialize handshake, ping, tools/list and
tools/call, JSON-RPC batches and errors, is here. The app’s servers:
SmarterProjectMCPServer: the public server of The Smarter Project’s repositories, documentation and blog.SmarterOperationsMCPServer: the staff-only server of this platform’s operations: its logs, its health and its users.
The server speaks the initialize handshake of every protocol version that the MCP Python SDK
negotiates (mcp_types.version.HANDSHAKE_PROTOCOL_VERSIONS):
initialize: the server’s identity, its tools capability and its instructions.notifications/initializedand every other notification: accepted, and ignored.ping: an empty result.tools/list: the server’s tools.tools/call: runs a tool. A tool’s own errors, e.g. a file that does not exist, are returned as a result withisErrorset, so that the LLM can correct its call.
Why not the MCP SDK’s server. The SDK’s server runs as an ASGI application, with an
anyio task group for its lifetime, and calls its tools as coroutines. Smarter’s views and
ORM are synchronous, and the platform runs under both the Django development server and
uvicorn. Every message is answered from the platform’s own data, with no state between
requests, so a server is a plain Django view that uses the SDK’s own Pydantic models,
mcp_types, to validate the requests and to build the responses.
Stateless. A server answers every POST with application/json, and does not issue an
Mcp-Session-Id. Clients therefore open no GET stream, and send no DELETE, and any of the
platform’s pods can answer any request.
Signals. Every notification and request that a server accepts sends
mcpserver_called, and every tool call sends
mcpserver_tool_called. The app’s receivers log them.
Note
Experimental. The MCPServer was designed and coded by Claude Code (Anthropic’s Claude Opus 5.5), with Lawrence McDaniel as co-author. It is experimental, and will be documented.
- smarter.apps.mcpserver.protocol.base.JsonRpcResponse = mcp_types.jsonrpc.JSONRPCResponse | mcp_types.jsonrpc.JSONRPCError
a response with a result, or an error.
- Type:
The answer to one JSON-RPC request
- smarter.apps.mcpserver.protocol.base.MAX_BATCH_SIZE = 20
The maximum number of messages in a JSON-RPC batch, which protocol 2025-03-26 allows.
A larger batch, or an empty one, is refused with one
INVALID_REQUESTerror, so that one POST cannot make a server run an unbounded number of tool calls.
- class smarter.apps.mcpserver.protocol.base.MCPServerBase[source]
Bases:
ABCAn MCP server: answers the JSON-RPC messages of one POST request with its
tools.A server lives for one request: the view creates it, calls
handle()with the request’s body, and returns what it answers. A subclass setstoolsin its constructor, and implementsname,server_info()andinstructions().Methods:
handle(): answers a request’s body, one message or a batch.handle_message(): answers one message, with a JSON-RPC response or error.dispatch(): runs one JSON-RPC method:initialize,ping,tools/listortools/call.error()anddump(): build a JSON-RPC error, and serialize a response. The views also use them, for errors that they answer before a server is created.
- call_tool(params)[source]
Run a tool.
A
SmarterMCPServerToolErrorthat the tool raises is returned as the result’s text, withis_errorset, so that the LLM can read it and correct its call. Sendsmcpserver_tool_calledafter every call, whether or not it failed.- Parameters:
params (
CallToolRequestParams) – The tool’s name and arguments.- Return type:
CallToolResult- Returns:
The tool’s result, as text. A tool error is a result with
is_errorset.- Raises:
LookupError – If there is no such tool.
- dispatch(method, params)[source]
Run one JSON-RPC method.
- Parameters:
- Return type:
Result- Returns:
Its result.
- Raises:
LookupError – If the server does not implement the method, or the tool does not exist.
ValidationError – If the params are not valid.
- static dump(response)[source]
Return a response as JSON.
Fields that are None are left out of a result, but an error keeps its
id, which JSON-RPC requires, even when it is null.- Parameters:
response (
JSONRPCResponse|JSONRPCError) – The response or error.- Return type:
TypeAliasType- Returns:
The response as a JSON-serializable dict, with the protocol’s camelCase names.
- static error(request_id, code, message)[source]
Return a JSON-RPC error response.
- Parameters:
- Return type:
JSONRPCError- Returns:
The error response. Serialize it with
dump().
- property formatted_class_name: str
The module, class and server name, formatted for the start of a log message.
- handle(payload)[source]
Answer the JSON-RPC message, or batch of messages, of one POST request.
A batch is a list of 1 to
MAX_BATCH_SIZEmessages, and is answered with a list of the responses of the messages that need one, in their order. A batch of notifications only needs no response.- Parameters:
payload (
TypeAliasType) – The request body, parsed from JSON.- Return type:
TypeAliasType|None- Returns:
The response body, or None if no message needs a response, e.g. a notification.
- handle_message(message)[source]
Answer one JSON-RPC message.
Sends
mcpserver_calledfor each valid notification and request. An exception that a method raises is answered with a JSON-RPC error: invalid params withINVALID_PARAMS, an unknown method or tool withMETHOD_NOT_FOUND, and any other exception withINTERNAL_ERROR, which is logged with its traceback, but whose message is not returned, so that internal details never reach the client.- Parameters:
message (
TypeAliasType) – The message.- Return type:
JSONRPCResponse|JSONRPCError|None- Returns:
The response, or None for a notification, or for a response from the client.
- handle_notification(message)[source]
Accept a JSON-RPC notification, a message without an
id, and ignore it.The server keeps no state between requests, so a notification, e.g.
notifications/initialized, changes nothing. A valid notification sendsmcpserver_called, so that it is logged, and one that is not valid is dropped, because JSON-RPC never answers a notification, not even with an error.
- initialize(params)[source]
Answer the initialize handshake.
The server offers only the tools capability, and its list of tools never changes during a session, so
listChangedis false.- Parameters:
params (
InitializeRequestParams) – The client’s offer: its protocol version, capabilities and identity.- Return type:
InitializeResult- Returns:
The client’s protocol version, if this server speaks it, and otherwise the latest that it speaks, which the client may refuse.
- abstractmethod instructions()[source]
Return the server’s instructions, which MCP clients add to the LLM’s system prompt.
- Return type:
- Returns:
Plain text that tells the LLM what the server is for, and when to call each tool.
- abstractmethod server_info()[source]
Return the server’s identity, for the initialize result.
- Return type:
Implementation- Returns:
The server’s name, title, version, description and website.
- tools: dict[str, MCPToolBase]
The server’s tools, by their name, in the order that
tools/listlists them.
- class smarter.apps.mcpserver.protocol.base.MCPToolBase[source]
Bases:
ABCA tool of an MCP server: a name, a description, and a Pydantic model of its arguments.
The tool’s MCP
inputSchemais generated fromarguments_model, andcall()returns its result as Markdown text for the LLM. A subclass sets the class attributes, and implementscall(). The two families of tools areSmarterProjectTool, of the public Smarter Project MCP server, andSmarterOperationsTool, of the staff-only Smarter Operations MCP server.- arguments_model: ClassVar[type[BaseModel]]
The Pydantic model of the tool’s arguments, which validates them and generates the
inputSchema.
- abstractmethod call(arguments)[source]
Validate a tool call’s arguments, and run the tool.
- Parameters:
arguments (
dict[str,Any] |None) – The tool call’s arguments, as the client sent them, or None.- Return type:
- Returns:
The result, as Markdown.
- Raises:
SmarterMCPServerToolError – If the arguments are not valid, or the tool fails. The server returns the error’s message to the LLM as a result with
isErrorset.
- definition()[source]
Return the tool, as
tools/listlists it.Its annotations are hints for the MCP client: a read-only tool is also idempotent, and no tool of this app destroys anything.
- Return type:
Tool- Returns:
The tool’s name, title, description,
inputSchemaand annotations.
- description: ClassVar[str]
What the tool does, and when to call it.
The LLM reads it to decide which tool to call.
- input_schema()[source]
Return the tool’s MCP
inputSchema.The schema is generated from
arguments_model, and simplified withsimplify_schema(). MCP requires a schema of typeobject, andadditionalProperties: falsetells the LLM that it may not invent arguments.
- open_world: ClassVar[bool] = False
Whether the tool reaches beyond the platform’s own data, e.g. to GitHub.
- smarter.apps.mcpserver.protocol.base.SERVER_VERSION = '9999.9999.9999.dev9999'
the platform’s, e.g.
0.41.0.It is the
versionof the server’s identity in theinitializeresult, so that a client can tell which release of the platform answered it.- Type:
Every server’s version
- smarter.apps.mcpserver.protocol.base.invalid_arguments(tool_name, error)[source]
Return the tool error for arguments that are not valid, naming each problem, for the LLM to correct.
The error is returned to the LLM as a tool result with
isErrorset, rather than as a JSON-RPC error, so that it can read what was wrong and call the tool again. Each problem is the argument’s dotted path, orargumentsfor the arguments as a whole, then Pydantic’s message, e.g.invalid arguments for search_logs: limit: Input should be less than or equal to 200.- Parameters:
tool_name (
str) – The tool’s name, e.g.search_logs.error (
ValidationError) – Pydantic’s validation error of the arguments.
- Return type:
- Returns:
The error, for the caller to raise.
- smarter.apps.mcpserver.protocol.base.simplify_schema(schema)[source]
Simplify a Pydantic JSON schema for an LLM.
Pydantic’s schema carries what helps a form, not an LLM: a title for the model and for each field, the model’s docstring as its description, and
"default": nullfor each optional field. They cost tokens in everytools/listresult, and the docstring is written for developers, so they are removed. An optional field’sanyOf: [<type>, {"type": "null"}]becomes its type, which every MCP client understands.