Sample MCP Server + AI Hook Web Service
A sample MCP (Model Context Protocol) server and an AI Hook Web Service built with Node.js and TypeScript.
- MCP Server — exposes Tools, Resources, and Prompts to AI clients (Claude Desktop, Claude Code, etc.)
- AI Hook Web Service — HTTP service for logging AI usage events from Claude, Claude Code, OpenAI/Codex, OpenCode, and any other provider. Logs are kept in memory and persisted to a JSON file (no database required), with automatic cost estimation and a live web dashboard.
Table of Contents
- Project Structure
- Setup
- MCP Server
- Running the MCP Server
- MCP Core Primitives
- Connect to Claude Desktop
- AI Hook Web Service
- Running the Web Service
- Web Dashboard
- Logging Claude Code Usage
- Endpoints
- Sending Hooks
- Cost Estimation
- Query Logs
- View Stats
- Dependencies
Project Structure
sample-mcp-and-hook/
├── src/
│ ├── main.ts # MCP server entry point
│ ├── tools.ts # MCP Tools (greet, add)
│ ├── resources.ts # MCP Resources (config, user-profile)
│ ├── prompts.ts # MCP Prompts (explain-code, review-code)
│ └── web-server.ts # AI Hook Web Service entry point
├── public/
│ └── index.html # Static web dashboard (AI Hook Monitor)
├── scripts/
│ └── ai-hook.sh # Enriches Claude Code Stop hooks with token usage
├── .claude/
│ └── settings.json # Claude Code hooks + AI_HOOK_URL
├── .mcp.json # MCP server registration for Claude Code
├── logs/ # Persisted hook logs (ai-hooks.json, git-ignored)
├── dist/ # Compiled JavaScript (after build)
├── package.json
├── tsconfig.json
└── README.md
Setup
Prerequisites: Node.js 18+ (and jq + curl for the Claude Code hooks)
# Install dependencies
pnpm install
# Build TypeScript
pnpm build
MCP Server
Running the MCP Server
# Production
pnpm start:mcp
# Development (auto-rebuild on change)
pnpm dev
MCP Core Primitives
| Primitive | Purpose | Direction | Example Use Case |
|---|---|---|---|
| Tools | Execute actions | AI → Server | Calculations, API calls |
| Resources | Provide data | Server → AI | Config, documents, user data |
| Prompts | Template messages | Server → AI | Standardized instructions |
Tools (src/tools.ts)
greet— greets a user by nameadd— adds two numbers together
Resources (src/resources.ts)
config://app— static application configurationusers://{userId}/profile— dynamic user profile by ID
Prompts (src/prompts.ts)
explain-code— prompt template for explaining codereview-code— parameterized code review (language, focus area)
Connect to Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"sample-mcp": {
"command": "node",
"args": ["/absolute/path/to/sample-mcp-and-hook/dist/main.js"]
}
}
}
For Claude Code, the server is already registered in .mcp.json (relative path) and enabled in .claude/settings.local.json — no manual setup needed once you've run pnpm build.
AI Hook Web Service
A lightweight HTTP service that receives AI usage events, logs them to the console, and persists them to logs/ai-hooks.json. Logs survive server restarts — the file is loaded automatically on startup. Cost is estimated automatically from token counts, and a live dashboard is served at the root URL. No database needed.
Running the Web Service
# Production (build first)
pnpm build
pnpm start:hook
# Development (watches compiled output)
pnpm dev # terminal 1 — recompile on change
pnpm dev:hook # terminal 2 — restart server on change
Default port: 3000. Override with the PORT environment variable:
PORT=4000 pnpm start:hook
Log file:
logs/ai-hooks.json(auto-created, already in.gitignore). Logs persist across restarts — the file is loaded on startup and rewritten after every hook received. Up to 1000 most-recent entries are retained in memory.
Web Dashboard
Once the service is running, open the dashboard in your browser:
http://localhost:3000/
The static dashboard (public/index.html) visualizes logged events and aggregate stats per provider and model — color-coded by provider (Claude, Claude Code, OpenAI, OpenCode) and event type.
Logging Claude Code Usage
This repo is preconfigured to log its own Claude Code usage to the web service.
Step 1 — Start the web service (keep it running in a separate terminal):
pnpm build
pnpm start:hook
# AI Hook Web Service running on http://localhost:3000
Step 2 — No model config needed. The hooks auto-detect your model at runtime:
- On Stop events,
scripts/ai-hook.shreads the session transcript and extracts the exact model plus real token usage (input, output, cache read/write). - On SessionStart / UserPromptSubmit events (no token data available), an inline hook resolves the model from
.claude/settings.json→~/.claude/settings.json→ falls back to"claude-code".
Whatever model you have configured in Claude Code is tracked automatically — no manual configuration required.
The hooks and endpoint are already wired up in .claude/settings.json:
{
"env": {
"AI_HOOK_URL": "http://localhost:3000/api/hook/claudecode"
},
"hooks": {
"SessionStart": [ /* inline curl — logs session_start */ ],
"UserPromptSubmit": [ /* inline curl — logs prompt_submit */ ],
"Stop": [ { "command": "bash scripts/ai-hook.sh" } ]
}
}
Step 3 — Open Claude Code in this project directory as usual:
claude
Every session start, user prompt, and Claude response is logged automatically (asynchronously, so Claude Code is never blocked).
Step 4 — View the logs in the dashboard or via the API:
# All events from Claude Code
curl "http://localhost:3000/api/logs?provider=claudecode"
# Aggregated stats
curl http://localhost:3000/api/stats
Note: The web service must be running on the configured port before Claude Code starts. If the service is down, hooks fail silently (Claude Code is not affected).
Endpoints
| Method | Path | Description |
|---|---|---|
GET |
/ |
Static web dashboard |
POST |
/api/hook |
Receive a hook (provider auto-detected from body) |
POST |
/api/hook/:provider |
Receive a hook with explicit provider name |
GET |
/api/logs |
List logs (newest first) |
GET |
/api/stats |
Aggregate stats grouped by provider + model |
DELETE |
/api/logs |
Clear all logs |
GET |
/health |
Health check (log count, log file path, uptime) |
Sending Hooks
The service auto-detects the provider from the request body. You can also send hooks to /api/hook/:provider to set the provider explicitly.
Claude
curl -X POST http://localhost:3000/api/hook \
-H "Content-Type: application/json" \
-d '{
"provider": "claude",
"model": "claude-sonnet-4-6",
"type": "message",
"id": "msg_01abc",
"session_id": "sess_xyz",
"usage": {
"input_tokens": 1200,
"output_tokens": 350,
"cache_read_input_tokens": 800
}
}'
Console output (cost is auto-estimated from tokens):
[HOOK] 2026-07-02T06:00:00.000Z | claude | claude-sonnet-4-6 | in=1200 out=350 total=1550 cost=$0.008850 session=sess_xyz
OpenAI / Codex
curl -X POST http://localhost:3000/api/hook \
-H "Content-Type: application/json" \
-d '{
"provider": "openai",
"model": "gpt-4o",
"object": "chat.completion",
"id": "chatcmpl-abc",
"cost_usd": 0.00014,
"usage": {
"prompt_tokens": 500,
"completion_tokens": 200,
"total_tokens": 700
}
}'
OpenCode
# Using explicit provider path
curl -X POST http://localhost:3000/api/hook/opencode \
-H "Content-Type: application/json" \
-d '{
"model": "claude-3-5-sonnet",
"event": "completion",
"input_tokens": 900,
"output_tokens": 420,
"total_tokens": 1320,
"cost": 0.00031,
"duration_ms": 1850,
"session_id": "oc_sess_999"
}'
Generic / Custom Provider
Any JSON body with a provider field works:
curl -X POST http://localhost:3000/api/hook \
-H "Content-Type: application/json" \
-d '{
"provider": "my-custom-llm",
"model": "my-model-v1",
"event": "inference",
"input_tokens": 300,
"output_tokens": 150,
"cost_usd": 0.00005,
"user_id": "user_42",
"metadata": { "region": "ap-southeast-1" }
}'
Cost Estimation
When a hook does not include an explicit cost_usd (or cost), the service estimates it from token counts using built-in per-model pricing (input, output, cache-read, and cache-write rates per million tokens). Pricing is matched by substring against the model name, covering the Claude Opus/Sonnet/Haiku families (v3, v3.5, and v4).
- If an explicit cost is provided in the payload, it is used as-is.
- If no matching model pricing is found, cost is left unset.
- Estimated cost appears in the console output, the
cost_usdlog field, and the aggregated stats.
Adjust the MODEL_PRICING table in src/web-server.ts to add models or update rates.
Query Logs
# All logs (newest first, default limit 50, max 500)
curl http://localhost:3000/api/logs
# Filter by provider (exact match)
curl "http://localhost:3000/api/logs?provider=claude"
# Filter by model name (partial match)
curl "http://localhost:3000/api/logs?model=sonnet"
# Combine filters and set limit
curl "http://localhost:3000/api/logs?provider=openai&limit=10"
# Clear all logs
curl -X DELETE http://localhost:3000/api/logs
Response shape:
{
"total": 42,
"returned": 10,
"logs": [
{
"id": "log_1782972333250_96p2e",
"received_at": "2026-07-02T06:05:33.250Z",
"provider": "claude",
"model": "claude-sonnet-4-6",
"event": "message",
"session_id": "sess_xyz",
"tokens": {
"input": 1200,
"output": 350,
"cache_read": 800,
"total": 1550
},
"cost_usd": 0.00885,
"request_id": "msg_01abc",
"raw": { "...": "original payload" }
}
]
}
View Stats
curl http://localhost:3000/api/stats
Stats are grouped by provider + model and sorted by call count (descending).
Response shape:
{
"providers": ["claude", "openai", "opencode"],
"stats": [
{
"provider": "claude",
"model": "claude-sonnet-4-6",
"calls": 15,
"total_input_tokens": 18000,
"total_output_tokens": 5250,
"total_tokens": 23250,
"total_cache_read_tokens": 9600,
"total_cache_write_tokens": 0,
"total_cost_usd": 0.1328,
"total_duration_ms": 0
},
{
"provider": "openai",
"model": "gpt-4o",
"calls": 8,
"total_input_tokens": 4000,
"total_output_tokens": 1600,
"total_tokens": 5600,
"total_cache_read_tokens": 0,
"total_cache_write_tokens": 0,
"total_cost_usd": 0.00112,
"total_duration_ms": 0
}
]
}
Dependencies
| Package | Purpose |
|---|---|
@modelcontextprotocol/sdk |
Official MCP SDK |
express |
HTTP server for the AI Hook Web Service |
zod |
Schema validation for MCP tool inputs |
Learn More
License
ISC