hello-mcp-server
by pugazece
README.md
# hello-mcp-server
Simple MCP server — TypeScript ESM (`"type": "module"`), `tsx`, Express + **Streamable HTTP** transport.
> Streamable HTTP (`POST/GET/DELETE /mcp`) was picked for scale / larger audience.
> It is the current MCP standard; legacy SSE (`/sse` + `/messages`) is deprecated.
## Prereqs
Node >= 22.5 (uses built-in `node:sqlite`, no extra DB deps)
## Setup
```bash
npm install
cp .env.example .env # optional, default PORT=3081
```
## Run
```bash
npm run dev # tsx watch src/index.ts
npm start # tsx src/index.ts
npm run build && npm run start:prod # compiled dist/
```
Server:
- `POST/GET/DELETE http://localhost:3081/mcp`
- `GET http://localhost:3081/health`
- `GET http://localhost:3081/metrics` — counts, avg latency, per-tool stats
- `GET http://localhost:3081/traces?limit=50` — recent HTTP traces
- `GET http://localhost:3081/traces/:traceId` — HTTP + tool calls for one trace
## Try it
Health:
```bash
curl http://localhost:3081/health
```
MCP Inspector:
```bash
npx @modelcontextprotocol/inspector
# Transport: Streamable HTTP, URL: http://localhost:3081/mcp
# Call tool `hello` with { "name": "pugazh" }
```
## Structure
```
src/
index.ts # bootstrap only (config, listen, graceful shutdown)
config.ts # env: PORT, SQLITE_PATH, LOG_LEVEL, LOG_PRETTY
logger.ts # pino (pretty in dev, JSON in prod)
observability/
trace.ts # AsyncLocalStorage trace context
db.ts # node:sqlite init + schema
repository.ts # logHttp/logTool/getMetrics/listRecentTraces/getTraceDetail
index.ts # barrel
mcp/
server.ts # createMcpServer() - registers all tools
sessions.ts # McpSessionStore (sessionId -> transport map)
tools/
withTracing.ts # runTracedTool() shared wrapper
hello.ts # hello tool
observability.ts # get_metrics / list_traces / get_trace tools
http/
app.ts # createApp() - middleware + route wiring
middleware/ # trace.ts (X-Trace-Id), requestLogger.ts (pino-http)
routes/ # health.ts, observability.ts, mcp.ts
```
Rules: HTTP layer never touches SQLite directly (via `observability/`);
MCP tools never touch Express (via `runTracedTool`); `index.ts` only
bootstraps. Add a tool = new file in `mcp/tools/` + one line in
`mcp/server.ts`.
## MCP tools
| Tool | Input | Returns |
|---|---|---|
| `hello` | `{ name? }` | `Hello, {name or world}!` |
| `get_metrics` | — | HTTP + tool-call stats, per-tool / per-route breakdowns |
| `list_traces` | `{ limit? }` | Recent HTTP traces, newest first |
| `get_trace` | `{ trace_id }` | HTTP + tool calls for one trace |
From VS Code Copilot (Agent mode): `What do the server metrics say?` or
`Show me the trace for <traceId>`. From the Inspector: call them like any
tool. Plain-HTTP mirrors: `GET /metrics`, `/traces`, `/traces/:id`.
## Logs (pino + pino-pretty)
Every request prints a pretty line to the terminal in dev
(`src/logger.ts`, via `pino-http`; `req.id` is the `X-Trace-Id`, so terminal
lines join to the SQLite traces below). Tool calls log a `tool called` line,
session init/close and bad-session warnings are logged too.
```bash
LOG_LEVEL=debug npm run dev # trace/debug lines
LOG_PRETTY=false npm start # JSON lines (production style)
```
Dual sink: the terminal stays pretty in dev, and every line is also
appended as JSON to `LOG_FILE` (default `logs/app.log`, auto-created,
git-ignored) — same events, grep-able and tail-able:
```bash
tail -f logs/app.log
cat logs/app.log | python3 -c "import sys,json; [print(json.loads(l)['msg'], json.loads(l).get('traceId')) for l in sys.stdin]"
LOG_FILE=false npm run dev # terminal only
LOG_FILE=/tmp/mcp.jsonl npm start # custom path
```
Rotation is built in (`pino-roll`, no logrotate needed): the file rolls
`daily` or at `10m`, keeping 14 rotated files + the active one
(`logs/app.1.log`, `app.2.log`, …). Tune via `LOG_ROTATE_FREQUENCY`
(`daily`|`hourly`|`<ms>`), `LOG_ROTATE_SIZE` (`10m`, `500k`, `1g`),
`LOG_ROTATE_KEEP`. Shutdown flushes the file transport first, so no
trailing lines are lost.
Note: `GET /mcp` holds an SSE stream open, so its request line prints when
the stream closes — that is expected.
Why you saw nothing before: telemetry only went to SQLite; stdout only had
startup lines. Now both exist: stdout (pino) + SQLite (`/metrics`, `/traces`).
## Observability & traceability (`node:sqlite`)
Zero-dependency SQLite via `node:sqlite` (`src/observability/`, DB at `SQLITE_PATH`,
default `data/mcp.db`, WAL mode). Every request gets `X-Trace-Id`
(pass your own or one is generated); it propagates via `AsyncLocalStorage`
into MCP tool handlers, so HTTP + tool calls share one `trace_id`.
Tables: `http_requests`, `tool_calls`. New tools stay traceable via the
shared wrapper (`src/mcp/tools/withTracing.ts`):
```ts
const text = await runTracedTool("my_tool", { arg }, async () => {
return "result";
});
```
```bash
curl http://localhost:3081/metrics
curl http://localhost:3081/traces?limit=20
curl http://localhost:3081/traces/<traceId>
```
## Add tools
New file in `src/mcp/tools/` (see `hello.ts`) → register it in
`src/mcp/server.ts`. Zod defines the input schema. Always go through
`runTracedTool` so stdout + SQLite tracing stay automatic.
## Scale note
Stateful by default via `McpSessionStore` (`src/mcp/sessions.ts`). For
horizontal scale without sticky sessions, switch to stateless mode
(`sessionIdGenerator: undefined`, no store) — see comment there.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues