MCP Package
The @hazeljs/mcp package exposes your HazelJS tools as an MCP (Model Context Protocol) server over STDIO transport — making every @Tool()-decorated method instantly callable by Claude Desktop, Cursor, and any other MCP-compatible AI host.
What Is MCP?
The Model Context Protocol is an open standard that lets AI hosts (Claude Desktop, Cursor, Continue, etc.) discover and call backend tools without any custom integration code. Instead of writing a one-off plugin for each client, you implement MCP once and every host that speaks the protocol can use your tools automatically.
@hazeljs/mcp bridges your existing HazelJS agent tools and the MCP protocol in one createMcpServer() call.
Why @hazeljs/mcp?
| Without @hazeljs/mcp | With @hazeljs/mcp |
|---|---|
| Write a custom plugin per AI client | One MCP server works with all clients |
| Manually serialize function signatures | Tool schemas generated from @Tool() metadata |
| Build your own JSON-RPC layer | Full JSON-RPC 2.0 router built in |
| Handle process I/O and error recovery | Robust STDIO transport, no crashes on bad JSON |
| Re-implement your tools for each integration | Register once, expose everywhere |
Architecture
graph TD A["AI Host<br/>(Claude Desktop / Cursor)"] -->|"JSON-RPC 2.0 over STDIO"| B["@hazeljs/mcp<br/>createMcpServer()"] B --> C["HazelToolAdapter<br/>(fromRegistry)"] C --> D["ToolRegistry<br/>(@hazeljs/agent)"] D --> E["@Tool() Methods<br/>(your business logic)"] B --> F["StdioTransport<br/>(newline-delimited JSON)"] B --> G["JSON-RPC Router<br/>(initialize / tools/list / tools/call)"] style A fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff style B fill:#6366f1,stroke:#818cf8,stroke-width:2px,color:#fff style C fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff style D fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff style E fill:#f59e0b,stroke:#fbbf24,stroke-width:2px,color:#fff style F fill:#8b5cf6,stroke:#a78bfa,stroke-width:2px,color:#fff style G fill:#8b5cf6,stroke:#a78bfa,stroke-width:2px,color:#fff
Key Components
createMcpServer(options)— Factory that wires up the adapter, transport, and JSON-RPC router and returns anMcpServerinstance.HazelToolAdapter— ConvertsToolRegistrymetadata into MCPToolDefinitionobjects and dispatchesinvoke()calls.StdioTransport— Reads newline-delimited JSON fromstdin, dispatches requests concurrently, and writes responses tostdout. Handles malformed JSON without crashing.- JSON-RPC Router — Handles
initialize,initialized,ping,tools/list, andtools/callper the MCP spec.
Installation
npm install @hazeljs/mcp @hazeljs/agent
Enable decorators in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Quick Start — With @hazeljs/agent
The fastest way to build an MCP server is to define tools using @Tool() decorators on a class and register it with ToolRegistry.
import '';
import { Tool, ToolRegistry } from '@hazeljs/agent';
import { createMcpServer } from '@hazeljs/mcp';
// 1. Define tools with @Tool() decorators
class WeatherAgent {
@Tool({
description: 'Get the current weather for a city.',
parameters: [
{ name: 'city', type: 'string', description: 'City name', required: true },
],
})
async getWeather(input: { city: string }) {
// Replace with a real weather API call
return { city: input.city, temperature: 22, condition: 'Sunny' };
}
@Tool({
description: 'Get a 7-day forecast for a city.',
parameters: [
{ name: 'city', type: 'string', description: 'City name', required: true },
],
})
async getForecast(input: { city: string }) {
return { city: input.city, forecast: ['Sunny', 'Cloudy', 'Rain', 'Sunny', 'Sunny', 'Cloudy', 'Rain'] };
}
}
// 2. Register tools with the registry
const registry = new ToolRegistry();
registry.registerAgentTools('weather', new WeatherAgent());
// 3. Create and start the MCP server
const server = createMcpServer({ registry });
server.listenStdio();
// Listening on stdin/stdout. Connect any MCP host.
Quick Start — Standalone (No @hazeljs/agent)
You can also build an MCP server without the agent package by implementing the IToolRegistry interface directly — useful for simple tools or for integrating existing business logic:
import { createMcpServer, HazelToolAdapter } from '@hazeljs/mcp';
import type { IToolRegistry, HazelTool } from '@hazeljs/mcp';
// Implement the minimal IToolRegistry interface
class SimpleRegistry implements IToolRegistry {
private tools = new Map<string, HazelTool>();
register(tool: HazelTool) {
this.tools.set(tool.metadata.name, tool);
}
getAll(): HazelTool[] {
return [...this.tools.values()];
}
}
const registry = new SimpleRegistry();
// Register a tool manually
registry.register({
metadata: {
name: 'search_docs',
description: 'Search HazelJS documentation.',
parameters: [
{ name: 'query', type: 'string', description: 'Search query', required: true },
],
},
target: {},
method: async function (input: { query: string }) {
return { results: [`Result for: ${input.query}`] };
},
});
const server = createMcpServer({ registry });
server.listenStdio();
Real-World Example: Customer Support MCP Server
This example exposes four support tools — order lookup, ticket creation, knowledge base search, and customer profile — as an MCP server.
import '';
import { Tool, ToolRegistry } from '@hazeljs/agent';
import { createMcpServer } from '@hazeljs/mcp';
// ─── Mock data ────────────────────────────────────────────────────────────────
const orders: Record<string, { status: string; items: string[]; total: number }> = {
'ORD-001': { status: 'shipped', items: ['Blue T-Shirt', 'White Sneakers'], total: 89.99 },
'ORD-002': { status: 'processing', items: ['Laptop Stand'], total: 45.00 },
};
const knowledgeBase = [
{ id: 'kb-1', topic: 'returns', content: 'Returns accepted within 30 days with receipt.' },
{ id: 'kb-2', topic: 'shipping', content: 'Free shipping on orders over $50. Standard: 5-7 days.' },
];
// ─── Tool class ───────────────────────────────────────────────────────────────
class SupportAgent {
@Tool({
description: 'Look up an order by ID. Returns status, items, and total.',
parameters: [
{ name: 'orderId', type: 'string', description: 'Order ID (e.g. ORD-001)', required: true },
],
})
async lookupOrder(input: { orderId: string }) {
const order = orders[input.orderId];
if (!order) return { error: 'Order not found', orderId: input.orderId };
return { orderId: input.orderId, ...order };
}
@Tool({
description: 'Search the knowledge base for policies and FAQs.',
parameters: [
{ name: 'query', type: 'string', description: 'Search query', required: true },
],
})
async searchKnowledgeBase(input: { query: string }) {
const q = input.query.toLowerCase();
const results = knowledgeBase.filter(a =>
a.topic.includes(q) || a.content.toLowerCase().includes(q),
);
return { results, total: results.length };
}
@Tool({
description: 'Create a new support ticket for a customer issue.',
parameters: [
{ name: 'customerId', type: 'string', description: 'Customer ID', required: true },
{ name: 'subject', type: 'string', description: 'Ticket subject', required: true },
{ name: 'description', type: 'string', description: 'Detailed description', required: true },
],
})
async createTicket(input: { customerId: string; subject: string; description: string }) {
const ticketId = `TKT-${Date.now()}`;
return { ticketId, status: 'open', createdAt: new Date().toISOString(), ...input };
}
}
// ─── Server setup ─────────────────────────────────────────────────────────────
const registry = new ToolRegistry();
registry.registerAgentTools('support', new SupportAgent());
const server = createMcpServer({ registry });
server.listenStdio();
Build and run:
npx tsc && node dist/main.js
Connecting to AI Clients
Cursor
Add to .cursor/mcp.json (project-level) or ~/.cursor/mcp.json (global):
{
"mcpServers": {
"hazel-support": {
"command": "node",
"args": ["dist/main.js"],
"cwd": "/path/to/your/mcp-server"
}
}
}
Restart Cursor. Your tools appear in the Cursor MCP tool panel and are available to Claude in the Composer.
Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS):
{
"mcpServers": {
"hazel-support": {
"command": "node",
"args": ["/absolute/path/to/dist/main.js"]
}
}
}
Restart Claude Desktop. Your tools are listed in the tool picker.
Manual Testing with JSON-RPC
You can test the server directly from a terminal:
# Start the server
node dist/main.js
# In another terminal, pipe requests in:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | node dist/main.js
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | node dist/main.js
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"lookup_order","arguments":{"orderId":"ORD-001"}}}' | node dist/main.js
Protocol Details
@hazeljs/mcp implements the following MCP JSON-RPC 2.0 methods:
| Method | Description |
|---|---|
initialize | Handshake — exchanges protocol version and capabilities |
initialized | Notification acknowledging handshake completion |
ping | Keepalive — returns an empty {} result |
tools/list | Returns all registered tool definitions with JSON Schema parameters |
tools/call | Invokes a named tool with input arguments, returns the result |
Tool Definition Shape
Each tool is advertised to the host as an MCP ToolDefinition:
{
"name": "lookup_order",
"description": "Look up an order by ID. Returns status, items, and total.",
"inputSchema": {
"type": "object",
"properties": {
"orderId": {
"type": "string",
"description": "Order ID (e.g. ORD-001)"
}
},
"required": ["orderId"]
}
}
API Reference
createMcpServer(options)
Creates an MCP server and returns an McpServer instance.
interface McpServerOptions {
registry: IToolRegistry; // Provides the list of HazelTool entries
name?: string; // Server name advertised in initialize (default: 'hazel-mcp')
version?: string; // Server version (default: '1.0.0')
}
interface McpServer {
listenStdio(): void; // Start reading stdin / writing stdout
listTools(): McpToolDefinition[]; // Return all registered tool definitions
}
HazelToolAdapter
Low-level adapter used internally by createMcpServer. Use directly for advanced scenarios:
import { HazelToolAdapter } from '@hazeljs/mcp';
const adapter = HazelToolAdapter.fromRegistry(registry);
// List all tool definitions (JSON Schema)
const tools = adapter.listTools();
// Check if a tool exists
if (adapter.hasTool('lookup_order')) {
// Invoke the tool
const result = await adapter.invoke('lookup_order', { orderId: 'ORD-001' });
console.log(result);
}
IToolRegistry Interface
If you are not using @hazeljs/agent, implement this interface to supply tools:
interface IToolRegistry {
getAll(): HazelTool[];
}
interface HazelTool {
metadata: {
name: string;
description: string;
parameters: Array<{
name: string;
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
description: string;
required: boolean;
}>;
};
target: object;
method: (...args: unknown[]) => unknown;
}
Error Codes
@hazeljs/mcp uses standard JSON-RPC 2.0 error codes plus MCP-specific extensions:
| Code | Constant | Description |
|---|---|---|
-32700 | PARSE_ERROR | Malformed JSON received |
-32600 | INVALID_REQUEST | Not a valid JSON-RPC request |
-32601 | METHOD_NOT_FOUND | Unknown MCP method |
-32602 | INVALID_PARAMS | Missing or invalid parameters |
-32603 | INTERNAL_ERROR | Unhandled server error |
-32000 | TOOL_NOT_FOUND | tools/call on an unregistered tool name |
Best Practices
Write Precise Tool Descriptions
The AI host reads your tool descriptions to decide which tool to call. Be specific about what the tool does, what valid parameter values look like, and what it returns.
// ✅ Clear and informative
@Tool({
description: 'Look up order status and items by order ID. Returns canRefund flag.',
parameters: [
{ name: 'orderId', type: 'string', description: 'Order ID in format ORD-XXX', required: true },
],
})
// ❌ Too vague — the AI may call it at the wrong time
@Tool({ description: 'Get order', parameters: [{ name: 'id', type: 'string', required: true }] })
Return Structured Objects
Return JSON objects, not plain strings. The host can display structured data more effectively and the AI can reason better about field-level information.
// ✅ Structured result
return { orderId, status: 'shipped', tracking: 'TRACK123', estimatedDelivery: '2026-03-10' };
// ❌ Flat string — loses structure
return `Order ${orderId} is shipped, tracking TRACK123`;
Handle Not-Found Gracefully
Return a structured error object rather than throwing. This lets the AI explain the problem to the user rather than seeing a server error.
async lookupOrder(input: { orderId: string }) {
const order = await db.findOrder(input.orderId);
if (!order) return { error: 'Order not found', orderId: input.orderId };
return order;
}
Keep the Server Process Light
The MCP server runs as a long-lived subprocess. Avoid heavy initialization at the top level — lazy-connect to databases on first use or use connection pooling.
Related
- Agent Package — Define tools with
@Tool()decorators and register withToolRegistry - AI Package — LLM providers (OpenAI, Anthropic, Gemini) used by agents
- RAG Package — Retrieve documents inside your MCP tools for knowledge-base answers
- Prompts Package — Manage and version LLM prompt templates used in MCP tools
For the full source, examples, and changelog, see the MCP package on GitHub.