Skip to main content
Glama
Suchanon

learn-typescript-mcp

by Suchanon
README.md
# learn TypeScript MCP

Learning monorepo for the [Model Context Protocol](https://modelcontextprotocol.io) TypeScript SDK, built on [Bun](https://bun.com) workspaces. Each learning topic lives in its own app under [apps/](apps/) — new topics get a new `apps/<topic>` workspace.

| App | What it is |
| --- | --- |
| [apps/weather/](apps/weather/) | The MCP weather tutorial, restructured as a **NestJS** application (running on the Bun runtime) |
| [apps/tax-assistant/](apps/tax-assistant/) | A minimal plain-TypeScript stdio MCP server (VAT calculator) |
| [apps/knowledge-base/](apps/knowledge-base/) | A stdio MCP server backed by `bun:sqlite`, covering tools + resources + a prompt (notes with tags) |

To install dependencies for all workspaces:

```bash
bun install
```

## Weather app (`apps/weather/`) — NestJS + MCP

Follows the official [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) "Build your first server" / "Build your first client" tutorials, using the real NWS weather API — hosted inside a NestJS app.

- [apps/weather/src/main.ts](apps/weather/src/main.ts) — Nest HTTP bootstrap (port 3000, `bodyParser: false`)
- [apps/weather/src/stdio.ts](apps/weather/src/stdio.ts) — stdio entry point (Nest application context, logger disabled so stdout stays protocol-only)
- [apps/weather/src/weather/weather.service.ts](apps/weather/src/weather/weather.service.ts) — NWS API calls (`getActiveAlertHeadlines`, `getForecastPeriods`)
- [apps/weather/src/weather/weather-mcp.service.ts](apps/weather/src/weather/weather-mcp.service.ts) — builds the `McpServer`, registers `get-alerts` / `get-forecast`, owns the shared HTTP transport
- [apps/weather/src/weather/mcp.controller.ts](apps/weather/src/weather/mcp.controller.ts) — routes every method on `/mcp` into the MCP transport
- [apps/weather/src/weather/express-web-bridge.ts](apps/weather/src/weather/express-web-bridge.ts) — converts Express req/res ↔ web-standard Request/Response (the SDK's v2 transport is web-standard only)
- [apps/weather/client.ts](apps/weather/client.ts) — example client (spawns the stdio server itself)
- [apps/weather/test/mcp-http.test.ts](apps/weather/test/mcp-http.test.ts) — `bun test` boots the Nest app on an ephemeral port and connects a real streamable-HTTP MCP client
- [tutorial/weather-server.ts](tutorial/weather-server.ts) — compatibility shim for clients still pointing at the old pre-monorepo path (e.g. a saved MCP Inspector connection); safe to delete once nothing references it

Both tools declare `default` values in their input schemas for development convenience — Inspector pre-fills its form with them (`state: CA`, latitude/longitude: San Francisco), so you can hit "Run Tool" without typing. Callers that pass their own arguments override them; the defaults only apply when an argument is omitted.

### Run the stdio server

Each client that connects (Claude Code, Inspector, `client.ts`) spawns its **own private copy** of this process — it's not a shared server.

```bash
bun run weather:stdio
```

### Run the HTTP server

One shared process that clients connect to over the network. Kill this terminal and every connected client loses the connection immediately.

```bash
bun run weather:http
```

### Run the example client

```bash
bun run weather:client
```

### Run the tests / typecheck

```bash
bun test          # from apps/weather/ (or the repo root)
bun run typecheck # from the repo root — tsc over both apps
```

## Tax assistant (`apps/tax-assistant/`)

Minimal stdio MCP server with a single `calculate-vat` tool (Thai VAT 7%):

```bash
bun run tax:stdio
```

## Knowledge base (`apps/knowledge-base/`)

Persistent notes-with-tags server backed by `bun:sqlite` ([apps/knowledge-base/src/db.ts](apps/knowledge-base/src/db.ts), [apps/knowledge-base/src/notes.ts](apps/knowledge-base/src/notes.ts)). Unlike weather/tax-assistant, this app exercises all three MCP primitives:

- **Tools** (actions a client explicitly calls): `add-note`, `get-note`, `search-notes`, `list-notes`, `delete-note`, `add-tags`, `list-tags`
- **Resources** (data a client can pull into context without a tool call): `kb://notes` (JSON index of every note) and the `kb://notes/{id}` template (one note as markdown)
- **Prompts** (reusable message templates a client can surface as a menu item): `summarize-notes-by-tag`, which embeds matching notes' content into the generated prompt

The SQLite file (`apps/knowledge-base/knowledge-base.sqlite`, plus its `-wal`/`-shm` sidecars) is created on first run and gitignored — each clone starts with an empty knowledge base.

```bash
bun run kb:stdio
```

## Inspect/test with MCP Inspector

```bash
bunx @modelcontextprotocol/inspector
```

Opens a browser UI (proxy on port 6277, UI on port 6274). In the sidebar, connect it either way:

- **stdio**: Transport Type `STDIO`, Command `bun`, Args `apps/weather/src/stdio.ts`
- **HTTP**: Transport Type `Streamable HTTP`, URL `http://localhost:3000/mcp` — requires the HTTP server (`bun run weather:http`) to already be running

Inspector remembers your last-used connection in the browser and auto-reconnects with it on load/refresh — always check the sidebar's Transport Type/URL before assuming what it's actually connected to.

To switch between this repo's stdio servers without retyping Command/Args each time, use [mcp.inspector.json](mcp.inspector.json) (same `mcpServers` shape as [.mcp.json](.mcp.json)):

```bash
bunx @modelcontextprotocol/inspector --config mcp.inspector.json --server knowledge-base
# or --server weather / --server tax-assistant
```

There's no in-UI dropdown to swap servers from a config file — picking a different one means relaunching with a different `--server` value.

### Inspecting multiple servers at once

One Inspector instance connects to exactly one server. To inspect two or three at the same time, run separate instances on separate ports (`CLIENT_PORT`/`SERVER_PORT`) and open each in its own browser tab:

```bash
# terminal 1
CLIENT_PORT=6274 SERVER_PORT=6277 bunx @modelcontextprotocol/inspector --config mcp.inspector.json --server weather

# terminal 2
CLIENT_PORT=6280 SERVER_PORT=6281 bunx @modelcontextprotocol/inspector --config mcp.inspector.json --server tax-assistant

# terminal 3
CLIENT_PORT=6282 SERVER_PORT=6283 bunx @modelcontextprotocol/inspector --config mcp.inspector.json --server knowledge-base
```

Each opens its UI on its own port (`http://localhost:6274`, `:6280`, `:6282`) — leaving `CLIENT_PORT`/`SERVER_PORT` unset on more than one instance will collide on the 6274/6277 defaults.

If you get `PORT IS IN USE` on 6274/6277, a previous Inspector instance didn't shut down cleanly (it can leave its proxy process orphaned even after reporting failure). Find and kill it before retrying:

```bash
lsof -nP -iTCP:6274,6277 -sTCP:LISTEN
kill <PID>
```

## Connect to Claude Code

[.mcp.json](.mcp.json) registers the `weather` server, pointed at the HTTP transport — start the Nest server (`bun run weather:http`) yourself before opening a Claude Code session here, or `/mcp` will show it disconnected.

## NestJS-on-Bun notes

- No `nest-cli`/webpack build step — Bun runs the TypeScript entrypoints directly (`bun src/main.ts`).
- [tsconfig.json](tsconfig.json) enables `experimentalDecorators` + `emitDecoratorMetadata`; Bun's transpiler honors both, which is what makes Nest constructor injection work.
- Nest still uses its Express adapter internally; only the runtime and package manager are Bun.