mcp-hello-typescript-server
by mitchallen
README.md
# mcp-hello-typescript-server
[](https://github.com/mitchallen/mcp-hello-typescript-server/actions/workflows/ci.yml) [](https://github.com/mitchallen/mcp-hello-typescript-server/actions/workflows/image-scan.yml) [](https://github.com/mitchallen/mcp-hello-typescript-server/actions/workflows/npm-audit.yml) [](https://github.com/mitchallen/mcp-hello-typescript-server/actions/workflows/publish.yml)
[](https://hub.docker.com/r/mitchallen/mcp-hello-typescript-server) [](https://hub.docker.com/r/mitchallen/mcp-hello-typescript-server/tags) [](https://hub.docker.com/r/mitchallen/mcp-hello-typescript-server) [](https://github.com/mitchallen/mcp-hello-typescript-server/pkgs/container/mcp-hello-typescript-server) [](#license)
A minimal [MCP](https://modelcontextprotocol.io) server built with **TypeScript**
and the official
[`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk)
— a good starting point for a new server or a demo. It exposes just two tools:
- **`server_info`** — a health/status check.
- **`greet`** — a friendly greeting in one of a handful of languages, defaulting
to English. Ask it to "greet in French" and it replies `Bonjour!`.
Built with **TypeScript**, the **[TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)**,
and **make**. It is the TypeScript port of the sibling Python
[`mcp-hello-server`](../mcp-hello-server), following the official MCP
[Build a server (TypeScript)](https://modelcontextprotocol.io/docs/develop/build-server#typescript)
reference. The Docker image compiles the TypeScript and runs it on a distroless
Chainguard/Wolfi Node base — no shell, no package manager, non-root.
---
## Quick start — demo an MCP server in 2 minutes
New to MCP? This is a tiny, safe server for **seeing how an MCP client discovers
and calls tools**. Every tool is a harmless in-memory lookup, so it's a good
sandbox. All you need is **[Docker](https://docs.docker.com/get-docker/)** and an
MCP client — the steps below use **[Claude Code](https://claude.com/claude-code)**
and the published Docker image (nothing to build or install).
> **Already running one of the sibling hello servers?** The Python
> [`mcp-hello-server`](../mcp-hello-server) (alias `hello`), Go
> [`mcp-hello-go-server`](../mcp-hello-go-server) (alias `hello-go`), and Rust
> [`mcp-hello-rust-server`](../mcp-hello-rust-server) (alias `hello-rust`) expose
> the same `server_info` / `greet` tools, so it's easy to test the wrong one.
> Remove any you don't want registered so your client only talks to `hello-ts`:
>
> ```sh
> claude mcp list # see what's registered
> claude mcp remove hello # the Python server, if present
> claude mcp remove hello-go # the Go server, if present
> claude mcp remove hello-rust # the Rust server, if present
> ```
**1. Add the server.** Claude Code launches the container per session and talks
to it over stdio:
```sh
claude mcp add hello-ts -- docker run -i --rm -e MCP_TRANSPORT=stdio ghcr.io/mitchallen/mcp-hello-typescript-server:latest
```
**2. Confirm it connected:**
```sh
claude mcp list # "hello-ts" should report ✔ Connected
```
**3. Ask in plain language** — Claude discovers the tools and picks one (the tool
it calls is in parentheses):
- "Is the hello server up? What version is it?" → (`server_info`)
- "Greet me in French." → (`greet` → **Bonjour!**)
- "Say hello in Japanese to Alice." → (`greet` → **こんにちは (Konnichiwa), Alice!**)
- "What languages can you greet in?" → (`server_info`, reads `languages`)
That round trip — the client listing tools, then calling one with arguments and
getting structured JSON back — _is_ MCP.
**4. Remove it when you're done:**
```sh
claude mcp remove hello-ts
```
> **Prefer HTTP?** Run it as a long-lived server instead:
>
> ```sh
> docker run --rm -p 8000:8000 ghcr.io/mitchallen/mcp-hello-typescript-server:latest
> claude mcp add --transport http hello-ts http://localhost:8000/mcp
> ```
---
## Tools
| Tool | Purpose |
| ------------------------- | ------------------------------------------------------------- |
| `server_info()` | Health/status: app name, version, uptime, supported languages |
| `greet(language?, name?)` | Greeting in `language` (default English); optional `name` |
### `greet`
`greet` takes two optional arguments:
- **`language`** — a language name, an alternate spelling, or an ISO code
(case-insensitive). Omit it to default to English. Supported: `english`,
`spanish`, `french`, `german`, `italian`, `portuguese`, `japanese`,
`hawaiian` (e.g. `french`, `Français`, or `fr` all work).
- **`name`** — optional; personalizes the message (`Bonjour, Alice!`).
It returns `{ language, greeting, message }`:
```jsonc
// greet(language="french")
{ "language": "french", "greeting": "Bonjour", "message": "Bonjour!" }
// greet(language="spanish", name="Alice")
{ "language": "spanish", "greeting": "Hola", "message": "Hola, Alice!" }
// greet() -> { "language": "english", "greeting": "Hello", "message": "Hello!" }
```
An unknown language returns a tool error listing the supported set.
### Add a language
Add a row to `GREETINGS` in `src/greetings.ts` (and, optionally, an alias / ISO
code to `ALIASES`). `server_info` reports the supported set automatically.
---
## Quick start (from source)
Requires [Node.js](https://nodejs.org/) 20+.
```sh
make install # npm ci
make build # tsc -> ./build
make test # run the test suite
make run # run the server over stdio
```
`make help` lists every target.
---
## Running the server
### stdio (default — for MCP clients that launch the server)
```sh
npm run dev # runs src/index.ts via tsx
# or, after `make build`:
node build/index.js
# or
make run
```
### Streamable HTTP (for networked clients / containers)
```sh
make run-http # PORT defaults to 8000
PORT=9000 make run-http
```
The MCP endpoint is served at `/mcp`.
---
## Configuration
All configuration is via environment variables:
| Variable | Default | Purpose |
| --------------- | ----------------------------- | ------------------------------ |
| `APP_NAME` | `mcp-hello-typescript-server` | Name reported by `server_info` |
| `MCP_TRANSPORT` | `stdio` | `stdio` or `http` |
| `HOST` | `127.0.0.1` | Bind address for `http` |
| `PORT` | `8000` | Bind port for `http` |
---
## Using with an MCP client — local development (from source)
Point a stdio-based client (e.g. Claude Desktop, Claude Code) at the built
entry point. With Claude Code, from the project directory:
```sh
make build
claude mcp add hello-ts -- node "$PWD/build/index.js"
```
Confirm it's connected with `claude mcp list` (or `/mcp` inside a session).
### Example prompts (Claude Code)
Once the server is added, just ask in plain language — Claude picks the right
tool. The tool it invokes is shown in parentheses.
- "Is the hello server up? What version is it?" → (`server_info`)
- "Greet me." → (`greet`, defaults to English → "Hello!")
- "Greet in French." → (`greet` with `language="french"` → "Bonjour!")
- "Say hello in Japanese to Alice." → (`greet` with `language="japanese"`, `name="Alice"`)
- "What languages can you greet in?" → (`server_info`, then read `languages`)
---
## Using a published image
The image is published to two registries:
- **GitHub Container Registry:** `ghcr.io/mitchallen/mcp-hello-typescript-server`
- **Docker Hub:** `mitchallen/mcp-hello-typescript-server`
### Option A — Docker image, client launches it (stdio)
This is the simplest setup: **there's nothing to build or install** — just the
published image. Pull it up front once so the first session doesn't block on the
download (which can race an MCP client's connect/startup timeout):
```sh
docker pull ghcr.io/mitchallen/mcp-hello-typescript-server:latest
```
The client starts a fresh container per session and talks to it over stdio. Use
`-i` (keep stdin open) and force the stdio transport, since the image defaults to
HTTP:
```jsonc
{
"mcpServers": {
"hello-ts": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"MCP_TRANSPORT=stdio",
"ghcr.io/mitchallen/mcp-hello-typescript-server:latest",
],
},
},
}
```
Claude Code equivalent:
```sh
claude mcp add hello-ts -- docker run -i --rm -e MCP_TRANSPORT=stdio ghcr.io/mitchallen/mcp-hello-typescript-server:latest
```
(Pin a version like `:0.1.0` in place of `:latest` for a reproducible setup.)
### Option B — Long-running container over HTTP
The image serves HTTP by default. Start it once, then point an HTTP-capable
client at it:
```sh
docker run -d --rm -p 8000:8000 --name mcp-hello-ts ghcr.io/mitchallen/mcp-hello-typescript-server:latest
claude mcp add --transport http hello-ts http://localhost:8000/mcp
```
For clients that only speak **stdio**, bridge to the HTTP endpoint with
[`mcp-remote`](https://www.npmjs.com/package/mcp-remote):
```jsonc
{
"mcpServers": {
"hello-ts": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://localhost:8000/mcp"],
},
},
}
```
Notes for remote use:
- Prefer **HTTPS** so traffic is encrypted in transit.
- This server ships **no authentication**. If you expose it beyond localhost, put
it behind a reverse proxy, gateway, or network policy.
- The endpoint path is `/mcp`.
### Option C — npm package via `npx` (not implemented here)
Because the server already exposes a `bin` entry point over stdio, it could also
be distributed as an **npm package** and launched with `npx` — the usual pattern
for MCP servers in Claude Code:
```sh
claude mcp add hello-ts -- npx -y <package-name>
```
Publishing to either the **public npm registry** (npmjs.com — zero-config `npx`)
or **GitHub Packages** (scoped `@owner/…`, but consumers need a token in
`.npmrc`) would enable this. It's left out of this demo on purpose to keep the
focus on the Docker distribution path.
---
## Docker
Published multi-platform (`linux/amd64`, `linux/arm64`) images run the server
over **streamable HTTP** by default (`MCP_TRANSPORT=http`, `HOST=0.0.0.0`,
`PORT=8000`) so they're reachable on a published port.
A multi-stage build compiles the TypeScript on
`cgr.dev/chainguard/node:latest-dev`, prunes to production dependencies, and
copies `build/` + `node_modules` onto a distroless
**[Chainguard/Wolfi](https://images.chainguard.dev) `node` base** — no shell, no
package manager, runs as the non-root `node` user. Unlike the Go/Rust siblings
(which ship a single static binary at ~10–17 MB), this image carries the Node
runtime and `node_modules`, so it's larger (~275 MB) — that's inherent to
shipping a runtime rather than a compiled binary. Every build is gated by a Trivy
scan (fails on fixable CRITICAL/HIGH); the dependency tree is separately scanned
with `npm audit`, and the published `:latest` is re-scanned daily — see
[Security scanning](#security-scanning).
### Pull and run
```sh
docker pull ghcr.io/mitchallen/mcp-hello-typescript-server:latest
docker run --rm -p 8000:8000 --name mcp-hello-ts ghcr.io/mitchallen/mcp-hello-typescript-server:latest
```
Then connect an HTTP MCP client to `http://localhost:8000/mcp`.
### Test a published release with make
```sh
make docker-test # up + smoke + down in one shot (exits non-zero on failure)
make docker-up # pull + run ghcr.io/mitchallen latest, detached
make docker-smoke # MCP `initialize` handshake — passes if the server responds
make docker-down # stop it
make docker-up TAG=0.1.0 # pin a version
make docker-up REGISTRY=docker.io/mitchallen # pull from Docker Hub instead
make docker-up HTTP_PORT=9000 # publish on a different host port
```
### Build locally
```sh
make docker-build # docker build -t mcp-hello-typescript-server .
make docker-run # serves http on localhost:8000
make scan # Trivy scan of the local image (fixable CRITICAL/HIGH fail)
```
---
## Security scanning
Two complementary gates catch vulnerabilities, both reproducible locally:
- **`image-scan`** (`make scan`) — Trivy scans the built container image and
fails the build on **fixable** CRITICAL/HIGH vulnerabilities. It covers the OS
layer of the runtime image and reads the JavaScript packages in
`node_modules`.
- **`npm-audit`** (`npm audit --omit=dev --audit-level=high`) — scans the
**production** dependency tree against the npm advisory database. Dev-only
tooling advisories don't wedge the build.
- **`scan-scheduled`** re-scans the published `:latest` image daily and uploads
results to the GitHub Security tab, catching CVEs disclosed after build time.
- **Dependabot** opens weekly PRs for npm packages, the Docker base image, and
GitHub Actions; low-risk updates auto-merge once CI passes.
---
## CI / Publish
Workflows live in `.github/workflows/`:
- **`ci`** — on every push/PR to `main`: prettier format check, `tsc` type-check,
and `node --test`.
- **`npm-audit`** / **`image-scan`** / **`scan-scheduled`** — vulnerability
scanning (see above).
- **`publish`** / **`publish-dockerhub`** — triggered by pushing a `v*` tag.
Build a multi-platform image, Trivy-scan it, push it to GHCR and Docker Hub,
then run `make docker-test` against the just-published image. The Docker Hub
job needs `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN` repository secrets.
To cut a release, use the `release` target — it bumps the `version` in
`package.json`, commits, tags, pushes, and creates the GitHub Release from the
`CHANGELOG.md` section, which triggers both publish workflows:
```sh
make release # patch bump (default)
make release BUMP=minor # or minor / major
```
The target refuses to run unless the working tree is clean, you're on `main`, and
`CHANGELOG.md` already has a `## [X.Y.Z]` section for the new version.
### Docker Hub secrets (one-time setup)
Pushing to **GHCR** needs no setup — it uses the built-in `GITHUB_TOKEN`. The
**`publish-dockerhub`** job additionally needs two repository secrets and a
pre-created Docker Hub repo:
1. **Create a Docker Hub access token** (not your password) with **Read & Write**
permissions, at hub.docker.com → Account Settings → Personal access tokens.
2. **Create the Docker Hub repository** `mitchallen/mcp-hello-typescript-server`
(Public).
3. **Add the two GitHub secrets** — `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN`:
```sh
gh secret set DOCKERHUB_USERNAME --body "mitchallen"
gh secret set DOCKERHUB_TOKEN # prompts for the value — paste the token
```
Without these, the GHCR `publish` job still succeeds; only `publish-dockerhub`
fails at the login step.
---
## Development
- Source: `src/`
- `greetings.ts` — greeting data + language resolution (`greet`), unit-tested
- `server.ts` — `createServer()` + tools registered with `server.registerTool`
- `version.ts` — reads the version from `package.json` at runtime
- `index.ts` — the entry point; transport wiring (stdio / HTTP)
- Tests: `tests/server.test.ts` drives the tools through an **in-memory client**
(`InMemoryTransport.createLinkedPair`, no network/subprocess);
`tests/greetings.test.ts` unit-tests the resolver/builder. Run everything with
`make test`, or the full CI gate with `make check` (prettier + type-check +
test).
- **Dependencies:** `package.json` / `package-lock.json` are committed. Run
`npm install` after changing dependencies to refresh the lockfile.
---
## License
MIT © Mitch Allen
TDQS
A4.4/5.0
Scored across 2 tools
Disambiguation5/5
The two tools serve entirely different purposes: one for generating greetings and one for server health. There is no overlap or ambiguity.
Naming Consistency4/5
Both names use snake_case, but 'greet' is a verb while 'server_info' is a noun, breaking a strict verb_noun pattern. Still, the style is consistent and readable.
Tool Count3/5
With only 2 tools, the server feels minimal. For a greeting-focused server, this might suffice, but it borders on too few for a typical MCP server scope.
Completeness4/5
The tools cover the core functionality (greeting and server info) with no obvious gaps. However, a dedicated tool to list supported languages could be considered a minor missing piece.
Maintenance
ActivityActive
ResponsivenessNo issues