my-first-server
# My First MCP Server
A minimal [Model Context Protocol](https://modelcontextprotocol.io) server that
exposes a single tool, `get_current_time`, over **stdio**. Built with the
[TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk).
Use it as a learning reference for how an MCP server is structured and how it
connects to hosts like Claude Code and the Claude desktop app.
> 📚 See [`docs/transports.md`](docs/transports.md) for notes on stdio vs HTTP
> transports, "Streamable HTTP," and the **stateless** spec update (2026-07-28)
> that removed `Mcp-Session-Id` and the handshake.
## How it works
An MCP **host** (Claude Code, the Claude desktop app) launches this **server**
as a child process and talks to it over stdin/stdout using newline-delimited
JSON-RPC. The server advertises its tools; when the model decides to use one,
the host sends a `tools/call` request, the handler runs, and the result flows
back into the model's context.
```
Claude (host) <--- JSON-RPC over stdio ---> node dist/index.js (this server)
```
> **stdio gotcha:** stdout is the JSON-RPC channel. Never `console.log` in a
> stdio server — it corrupts the protocol stream. Log to stderr with
> `console.error` (visible via `claude --debug` or the Inspector).
Key files:
| File | Purpose |
|------|---------|
| `src/server.ts` | The tool definition (`registerTool`). Transport-agnostic — shared by both entrypoints. |
| `src/index.ts` | **stdio** entrypoint (what Claude Code / desktop launch). |
| `src/http.ts` | **Streamable HTTP** entrypoint (Express, stateless, `POST /mcp`). |
| `src/client.ts` | Demo **client** — connects to the HTTP server, lists + calls the tool. |
| `.mcp.json` | Registers the server with **Claude Code** (project scope, committed). Uses a **relative** path so it works on any machine. |
| `package.json` | `prepare` script auto-builds `dist/` on `npm install`. |
| `dist/` | Compiled output. Git-ignored — regenerated by the build. |
| `docs/transports.md` | Notes on stdio vs HTTP transports and the stateless spec update. |
---
## Setup
The Node version is pinned in `.nvmrc`. Select it first (installs it if needed):
```bash
nvm use # reads .nvmrc; run `nvm install` first if that version is missing
npm install # installs deps AND builds dist/ (via the "prepare" script)
```
`nvm use` + `npm install` is all a fresh clone needs before the server is
runnable.
---
## Use cases
### 1. Local development — fast edit/test loop
Run straight from TypeScript source, no build step, using `tsx`:
```bash
npm run dev
```
Or compile and run the built output (what the hosts actually execute):
```bash
npm run build # tsc -> dist/
npm start # node dist/index.js
```
Rebuild after editing `src/` if you're testing through a host, since hosts run
the compiled `dist/index.js`.
> Tip: for a zero-build inner loop, you can point `.mcp.json` at
> `"command": "npx", "args": ["tsx", "src/index.ts"]` so the host runs the
> TypeScript directly. Switch back to `dist/index.js` for a "production" run.
### 2. Run the HTTP server + demo client
The same tool, served over **Streamable HTTP** instead of stdio. Start the
server (it runs on its own; a host connects by URL), then run the client in a
second terminal:
```bash
npm run start:http # -> http://localhost:3000/mcp (or npm run dev:http)
npm run client # connects, lists tools, calls get_current_time
```
The server is **stateless** (no `Mcp-Session-Id`): each `POST /mcp` builds a
fresh server + transport. See `docs/transports.md` for why.
By default it replies over **SSE** so it can stream progress notifications
(see the `stream_load` tool). Set `MCP_JSON=1` for single plain-JSON replies
(easier for curl, but no streaming):
```bash
MCP_JSON=1 npm run start:http
curl -s -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```
To watch the raw SSE stream (progress events, then the final result), run the
server in default SSE mode and see the streaming example in
[`docs/transports.md`](docs/transports.md#10-streaming-in-practice-this-repo).
To connect **Claude Code** to the HTTP server instead of stdio:
```bash
claude mcp add --transport http my-http-server http://localhost:3000/mcp
```
### 3. Inspect & debug with the MCP Inspector
The official Inspector is a standalone host with a UI — the fastest way to see
your tools, call them, and read stderr logs without involving Claude:
```bash
npx @modelcontextprotocol/inspector node dist/index.js
```
It opens a local web UI where you can run the `initialize` handshake, browse
`tools/list`, and invoke `get_current_time` with arguments.
### 4. Test the raw protocol by hand
This is exactly what a host does — feed it the three startup messages and a call:
```bash
printf '%s\n%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_current_time","arguments":{"timezone":"Asia/Kolkata"}}}' \
| node dist/index.js 2>/dev/null
```
### 5. Connect to Claude Code
`.mcp.json` is committed, so Claude Code picks the server up automatically:
```bash
cd <this-project>
claude # start a session in the project dir
```
On first launch, approve the `my-first-server` prompt (remembered per project).
Then inside the session:
```
/mcp # verify: my-first-server -> connected, 1 tool
what time is it in Tokyo right now? # triggers get_current_time
```
Debug connection issues with:
```bash
claude --debug # shows the handshake and the server's stderr logs
```
### 6. Connect to the Claude desktop app
The desktop app's config is **machine-local** (not in this repo). On macOS it
lives at:
```
~/Library/Application Support/Claude/claude_desktop_config.json
```
Add an entry under `mcpServers` pointing at this machine's **absolute** path to
`dist/index.js`:
```json
{
"mcpServers": {
"my-first-server": {
"command": "node",
"args": ["/absolute/path/to/this/repo/dist/index.js"]
}
}
}
```
Then fully quit (Cmd+Q) and reopen the app — config is only read at startup.
Look for the tools icon and ask the same time question.
---
## After you push to GitHub
Committed: `src/`, `package.json`, `package-lock.json`, `tsconfig.json`,
`.mcp.json`, `README.md`, `.gitignore`.
Not committed (regenerated per machine): `node_modules/`, `dist/`, and the
Claude **desktop** config (it's outside the repo).
So a teammate cloning the repo just runs:
```bash
git clone <repo> && cd <repo>
npm install # builds dist/ automatically
claude # Claude Code auto-detects .mcp.json; approve once
```
The desktop app is the only piece they re-register locally (use case 6), because
its config uses an absolute path unique to their machine.
---
## Publishing to npm (`@srav/mcp-time-server`)
Publishing lets anyone add the server with a single `npx` line — no clone, no
build, no absolute paths.
**Prerequisites already in place:**
- `bin` maps `my-mcp-server → dist/index.js`, and `src/index.ts` starts with
`#!/usr/bin/env node`, so the published package is directly runnable.
- `files: ["dist"]` ensures the compiled output ships in the tarball. (Without
it, npm falls back to `.gitignore` — which excludes `dist/` — and would
publish an empty package.)
- `prepare` builds `dist/` automatically on `npm publish`.
- `publishConfig.access = "public"` publishes the scoped package publicly.
**Verify, then publish:**
```bash
npm pack --dry-run # confirm dist/index.js is in the tarball
npm login # once
npm publish # runs prepare (build) then uploads
```
**After publishing**, switch `.mcp.json` (or use `claude mcp add`) from the local
path to the published package:
```jsonc
// local dev (current — keep while iterating)
{ "command": "node", "args": ["dist/index.js"] }
// published (after npm publish)
{ "command": "npx", "args": ["-y", "@srav/mcp-time-server"] }
```
```bash
# or register from the CLI, user scope = available in every project
claude mcp add --scope user my-first-server -- npx -y @srav/mcp-time-server
```
`npx` downloads the package to its cache on first run, then executes the `bin`
entry — from there the stdio JSON-RPC flow is identical to running locally.
## Distribution models at a glance
| Model | `.mcp.json` command | When |
|-------|--------------------|------|
| Local path | `node dist/index.js` | You, developing on this machine |
| Cloned repo | `node dist/index.js` | Teammates who clone + `npm install` |
| Published npm | `npx -y @srav/mcp-time-server` | Anyone, anywhere — no clone needed |
TDQS
Scored across 2 tools
The two tools are completely unrelated in function—one returns time and the other simulates streaming—so there is zero ambiguity about which to choose. No overlap or boundary confusion exists.
Both tool names follow a clear snake_case verb_noun pattern: get_current_time and stream_load. The naming is predictable and consistent, even if the verbs are not from the same domain.
With only two tools, the set feels thin and barely qualifies as a server. It's on the borderline of the acceptable range for a minimal utility or demo server.
The tools do not belong to a shared domain, so there is no meaningful coverage to assess. Each tool is an isolated snippet with no related operations, leaving the server's purpose unclear and incomplete.