MCP Server Example
MCP Server Example
This example builds a small MCP server that lets an AI agent search and manage products in a database — the same domain as the REST API example, but exposed as tools an agent can discover and call instead of endpoints a frontend calls.
For the concepts behind what is happening here — the stateless protocol design, how attributes become JSON Schema, return values, and error handling — see the MCP Server Overview and Defining Tools. This page focuses on assembling those pieces into a working app.
Project Structure
app/
├── Config/
│ └── app.php
├── Controllers/
│ └── Assistant.php
├── Models/
│ └── Product.php
└── route.php1. The Product Model
Reuse the same model from the REST API example — an MCP server calls the same application code as any other controller:
<?php
namespace App\Models;
use Hazaar\DBI\Adapter;
class Product
{
protected Adapter $db;
public function __construct(?Adapter $db = null)
{
$this->db = $db ?? Adapter::create();
$this->db->exec('
CREATE TABLE IF NOT EXISTS product (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL
)
');
}
public function search(string $query): array
{
return $this->db->table('product')->find(['name' => ['$ilike' => "%{$query}%"]])->fetchAll();
}
public function find(int $id): array|false
{
return $this->db->table('product')->findOne(['id' => $id]);
}
public function create(string $name, float $price): int
{
$this->db->table('product')->insert(['name' => $name, 'price' => $price]);
return (int) $this->db->lastInsertId();
}
}2. The MCP Controller
Create app/Controllers/Assistant.php. Extend Hazaar\Controller\MCP and mark each method an agent should be able to call with #[Tool]. The #[Param] attribute documents each argument for the model — see Defining Tools for the full attribute reference:
<?php
namespace App\Controllers;
use App\Models\Product;
use Hazaar\Controller\MCP;
use Hazaar\MCP\Attribute\Param;
use Hazaar\MCP\Attribute\Tool;
class Assistant extends MCP
{
protected string $serverName = 'product-catalog';
protected string $serverVersion = '1.0.0';
protected ?string $instructions = 'Tools for searching and managing the product catalog.';
protected Product $products;
public function init(): void
{
$this->products = new Product();
}
#[Tool('Search products by name')]
public function searchProducts(
#[Param('Name fragment to search for')]
string $query,
): array {
return $this->products->search($query);
}
#[Tool('Fetch a single product by ID')]
public function getProduct(
#[Param('The product ID')]
int $id,
): array {
$product = $this->products->find($id);
if (false === $product) {
throw new \RuntimeException("No product with ID {$id}. Use searchProducts to find one.");
}
return $product;
}
#[Tool('Add a new product to the catalog')]
public function createProduct(
#[Param('Product name')]
string $name,
#[Param('Product price', minimum: 0)]
float $price,
): array {
$id = $this->products->create($name, $price);
return $this->products->find($id);
}
}A few things worth noting:
init()runs once per request before any tool executes, so it is the right place to build shared dependencies like$productsrather than repeatingnew Product()in every method.getProduct()throws rather than returning an error array. Thrown exceptions become tool errors the model can read and recover from — see Error handling.- Only
#[Tool]methods are published.init()and any other helpers stay private to the server.
3. Routing the Endpoint
MCP uses a single POST endpoint; the JSON-RPC method in the body decides what happens, not the URL:
<?php
// app/route.php
use App\Controllers\Assistant;
use Hazaar\Application\Router;
Router::post('/mcp', [Assistant::class, 'index']);4. Try It Out
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" }
}
}'Create a product:
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: createProduct' \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "createProduct",
"arguments": { "name": "Widget", "price": 9.99 },
"_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28" }
}
}'Then search for 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: searchProducts' \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "searchProducts",
"arguments": { "query": "widget" },
"_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28" }
}
}'MCP-Protocol-Version, Mcp-Method, and (on tools/call) Mcp-Name are optional — most real clients only carry this information in the JSON-RPC body — but if you do send one, its value must match the body or the server returns 400. See Protocol Reference for the full header and error contract.
5. Connecting an Agent
Point any MCP client at the endpoint. For Claude Code:
claude mcp add --transport http product-catalog https://your-app.example.com/mcpOnce connected, an agent can call searchProducts, getProduct, and createProduct directly, using the descriptions above to decide when each one applies.
6. Securing the Server
/mcp runs application code on behalf of whatever calls it, so treat it like any other public API endpoint:
- Apply middleware or authentication to the route the same way you would for a REST endpoint. MCP is stateless, so token-based auth fits naturally.
- Check authorization inside each tool — a model choosing to call
createProductis not itself permission to do so.
See Securing a server for the full checklist.
Next Steps
- MCP Server Overview — the stateless protocol design and how the server works end to end.
- Defining Tools — attributes, schema generation, return values, and runtime-registered tools.
- Protocol Reference — supported methods, headers, and error codes.
- Creating a REST API — expose the same data as conventional JSON endpoints.