MCP Server Overview
MCP Server Overview
The Model Context Protocol (MCP) is an open standard that lets LLM agents — Claude, and any other MCP-capable client — discover and call functionality you expose. Hazaar's MCP component turns ordinary controller methods into MCP tools, generating the JSON Schema an agent needs from your PHP type declarations.
You write this:
namespace App\Controllers;
use Hazaar\MCP\Attribute\Param;
use Hazaar\MCP\Attribute\Tool;
class Assistant extends \Hazaar\Controller\MCP
{
protected string $serverName = 'acme-crm';
protected string $serverVersion = '1.0.0';
#[Tool('Search the customer database by name or email')]
public function searchCustomers(
#[Param('Name or email fragment to search for')]
string $query,
#[Param('Maximum number of results')]
int $limit = 10,
): array {
return Customer::search($query, $limit);
}
}An agent sees a searchCustomers tool, knows query is a required string and limit an optional integer defaulting to 10, and can call it. You did not write a line of protocol code or a byte of JSON Schema.
A stateless protocol
Hazaar implements protocol revision 2026-07-28, which redesigned MCP around a stateless core. This matters because it is what makes MCP a natural fit for PHP:
- No sessions. The
Mcp-Session-Idheader is gone. There is no session store to configure, no sticky routing, no shared state between requests. - No handshake required. The modern per-request model carries protocol version and client capabilities in every request instead of negotiating them once up front.
- One POST per request. Each JSON-RPC message is an independent HTTP POST.
A conventional PHP deployment already works exactly this way: a request arrives, a process handles it, the process forgets everything. An MCP server built on Hazaar scales behind a plain round-robin load balancer with no further thought.
Earlier revisions
Real clients (claude.ai, Claude Desktop) still widely negotiate 2025-06-18 and 2025-11-25, so Hazaar accepts requests declaring either one alongside 2026-07-28. If a client opens with the legacy initialize / notifications/initialized handshake those revisions require, Hazaar answers it too — statelessly, with no session created. What is not implemented is the session/streaming machinery those revisions additionally allow: GET and DELETE still return 405 regardless of the negotiated version. See Protocol Reference.
Creating a server
Extend Hazaar\Controller\MCP and mark the methods you want published:
namespace App\Controllers;
use Hazaar\MCP\Attribute\Tool;
class Assistant extends \Hazaar\Controller\MCP
{
protected string $serverName = 'acme-crm';
protected string $serverVersion = '1.0.0';
protected ?string $instructions = 'Tools for querying the Acme customer database.';
#[Tool('Get the current server time as an ISO-8601 string')]
public function serverTime(): string
{
return (new \DateTime())->format(\DateTime::ATOM);
}
}Only methods carrying #[Tool] are published, so the controller can have as many ordinary public helpers as you like without exposing them to a model.
The instructions property is optional prose shown to the agent describing what the server is for as a whole — useful for steering a model toward the right tool.
Routing the endpoint
MCP uses a single endpoint that accepts POST. Register one route:
// route.php
use App\Controllers\Assistant;
use Hazaar\Application\Router;
Router::post('/mcp', [Assistant::class, 'index']);The action name is irrelevant — the server dispatches on the JSON-RPC method in the request body, not on the URL — but a route needs one, so index is as good as any.
Trying it out
With the application running, ask the server what it can do:
curl -s -X POST http://localhost:8080/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/list' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28" }
}
}'{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "searchCustomers",
"description": "Search the customer database by name or email",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Name or email fragment to search for" },
"limit": { "type": "integer", "description": "Maximum number of results", "default": 10 }
},
"required": ["query"]
}
}
],
"ttlMs": 300000,
"cacheScope": "public",
"resultType": "complete",
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "acme-crm", "version": "1.0.0" }
}
}
}Then call it:
curl -s -X POST http://localhost:8080/mcp \
-H 'Content-Type: application/json' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: searchCustomers' \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "searchCustomers",
"arguments": { "query": "acme" },
"_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28" }
}
}'Headers are optional, but validated when present
MCP-Protocol-Version, Mcp-Method, and (on tools/call) Mcp-Name are not required — plenty of real clients only ever put this information in the JSON-RPC body. But if a client (or an intermediary in front of it) does send one of these headers, its value must match the request body, or the server returns 400 with a HeaderMismatch error. That check exists to stop a gateway routing on one value while the server executes another; sending the headers is still recommended wherever you control the client. See the Protocol Reference.
Connecting an agent
Point any MCP client at the endpoint URL. For Claude Code:
claude mcp add --transport http acme-crm https://your-app.example.com/mcpWhere to next
- Defining Tools — attributes, schema generation, return values and error handling.
- Protocol Reference — supported methods, headers, error codes, security.