Sign inSign up

mitchallen/mcp-hello-typescript-server

By mitchallen

•Updated 2 days ago

Image
0

594

mitchallen/mcp-hello-typescript-server repository overview

⁠mcp-hello-typescript-server

ci image-scan npm-audit publish

Docker Hub image size Docker pulls GHCR License: MIT

A minimal MCP⁠ server built with TypeScript and the official @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⁠, and make. It is the TypeScript port of the sibling Python mcp-hello-server⁠, following the official MCP Build a 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⁠ and an MCP client — the steps below use 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⁠ (alias hello), Go mcp-hello-go-server⁠ (alias hello-go), and Rust 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:

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:

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:

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:

claude mcp remove hello-ts

Prefer HTTP? Run it as a long-lived server instead:

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

ToolPurpose
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 }:

// 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⁠ 20+.

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)
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)
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:

VariableDefaultPurpose
APP_NAMEmcp-hello-typescript-serverName reported by server_info
MCP_TRANSPORTstdiostdio or http
HOST127.0.0.1Bind address for http
PORT8000Bind 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:

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):

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:

{
  "mcpServers": {
    "hello-ts": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "-e",
        "MCP_TRANSPORT=stdio",
        "ghcr.io/mitchallen/mcp-hello-typescript-server:latest",
      ],
    },
  },
}

Claude Code equivalent:

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:

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⁠:

{
  "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:

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⁠ 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⁠.

⁠Pull and run
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
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
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:

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:

    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

Tag summary

Content type

Image

Digest

sha256:80e0f0102…

Size

68.3 MB

Last updated

2 days ago

docker pull mitchallen/mcp-hello-typescript-server