Skip to main content
Glama
pugazece

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.