MCP TypeScript Starter is a production-conscious foundation for building a Model Context Protocol server with TypeScript. It includes one typed example tool, stdio and Streamable HTTP transports, strict validation, tests, a hardened container, and automated GHCR publication.
Clone it, replace the example domain, and keep the infrastructure that real MCP servers need.
- Use this starter
- About
- Features
- MCP tools
- Tech stack
- Installation
- Configuration
- MCP client setup
- Customizing the starter
- Verification
- Limitations
- Contributing
- License
Click Use this template on GitHub to create a new MCP server with an independent Git history. After creating it, replace the example tool and update the project identity by following Customizing the starter.
Fork this repository when you want to contribute improvements back through a pull request. See Contributing before submitting changes.
If this starter helped you, consider giving the repository a star. It helps other TypeScript developers discover the project.
The starter demonstrates the complete path from a validated MCP tool definition to a client-visible structured result. The server uses the current modular MCP TypeScript SDK and Hono's Web-standard HTTP model rather than a custom server framework.
The default stdio transport is intended for local clients that launch the server as a child process. Streamable HTTP is stateless and creates a fresh MCP server for each request, so it can be replicated without shared session storage.
The example performs bounded in-memory work. There is no telemetry, application database, persistent storage, authentication, or external service dependency.
- Registers tools with strict Zod input and output schemas.
- Returns both human-readable content and typed structured content.
- Includes accurate MCP safety annotations.
- Supports stdio and stateless Streamable HTTP.
- Uses Hono with Host and Origin validation against DNS rebinding.
- Binds HTTP to loopback by default and requires an allowlist for other interfaces.
- Limits tool inputs and HTTP request bodies.
- Keeps stdout exclusive to MCP protocol messages in stdio mode.
- Handles SIGINT and SIGTERM with idempotent graceful shutdown.
- Runs as a non-root container with read-only-root-filesystem support.
- Tests configuration, stdio wiring, MCP behavior, Hono routes, and real HTTP traffic.
- Publishes multi-architecture images only after quality checks pass.
Echoes a validated message and optional string metadata. It is deliberately simple so the repository teaches MCP schemas, registration, annotations, and results without inventing a business domain.
Example input:
{
"message": "Hello, MCP!",
"metadata": {
"source": "example-client"
}
}Example structured output:
{
"message": "Hello, MCP!",
"metadata": {
"source": "example-client"
}
}Messages are limited to 10,000 characters. Metadata accepts at most 20 entries; keys are limited to 64 characters and values to 1,024 characters.
- Node.js 24+
- TypeScript with strict project rules
- Model Context Protocol TypeScript SDK 2
- Hono
- Zod
- Vitest
- pnpm
- Docker
- Node.js 24+ and pnpm 11 for local development.
- Docker and Docker Compose for container deployment.
The recommended HTTP deployment uses the published multi-architecture image:
ghcr.io/lukegskw/mcp-typescript-starter:latest
Download the Compose example and provide the hostname clients will use:
curl -O https://raw.githubusercontent.com/lukegskw/mcp-typescript-starter/main/compose.example.yaml
export MCP_ALLOWED_HOSTS='mcp.example.internal'
docker compose -f compose.example.yaml up -dThe Streamable HTTP and health endpoints will be available at:
http://<host>:3000/mcp
http://<host>:3000/healthz
To publish a different host port, set MCP_PUBLISHED_PORT. The application still uses
port 3000 inside the container.
The latest tag follows the newest successful build from the default branch. Use a
version or immutable sha-* tag for controlled deployment and rollback.
docker run -d \
--name mcp-typescript-starter \
--restart unless-stopped \
--read-only \
--user 10001:10001 \
--cap-drop ALL \
--security-opt no-new-privileges:true \
--tmpfs /tmp:size=16m,mode=1777 \
-e MCP_TRANSPORT=streamable-http \
-e MCP_HOST=0.0.0.0 \
-e MCP_ALLOWED_HOSTS=127.0.0.1,localhost,mcp.example.internal \
-p 3000:3000 \
ghcr.io/lukegskw/mcp-typescript-starter:latestgit clone https://github.com/lukegskw/mcp-typescript-starter.git
cd mcp-typescript-starter
docker buildx build --load -t mcp-typescript-starter:local .git clone https://github.com/lukegskw/mcp-typescript-starter.git
cd mcp-typescript-starter
pnpm install --frozen-lockfile
pnpm build
pnpm start -- --transport stdioFor local Streamable HTTP development:
MCP_TRANSPORT=streamable-http pnpm dev| Variable | Required | Default | Description |
|---|---|---|---|
MCP_TRANSPORT |
No | stdio |
stdio or streamable-http. |
MCP_HOST |
No | 127.0.0.1 |
HTTP bind address. |
MCP_PORT |
No | 3000 |
HTTP listening port. |
MCP_ALLOWED_HOSTS |
Outside loopback | None | Comma-separated Host and Origin hostname allowlist. |
The --transport command-line option overrides MCP_TRANSPORT. MCP_ALLOWED_HOSTS
contains hostnames, not URLs; include every hostname legitimate clients and health
checks use.
The server has no secrets in its example configuration. Add domain credentials through the deployment platform or environment, never as MCP tool arguments or committed files.
For a client that accepts Streamable HTTP server definitions:
mcp_servers:
starter:
url: http://127.0.0.1:3000/mcpFor a client that launches a local stdio server:
{
"mcpServers": {
"starter": {
"command": "node",
"args": [
"/absolute/path/to/mcp-typescript-starter/dist/main.js",
"--transport",
"stdio"
]
}
}
}To let a local client launch the container over stdio, use docker run -i --rm and pass
--transport stdio after the image name. -i is required so the client can exchange
MCP messages through standard input and output.
Client configuration formats differ. Consult the client's documentation for its exact schema and restart or reload the client after changing its server definition.
The main extension points are intentionally direct:
- Copy or replace
src/tools/echo.ts. - Define strict input and output schemas before writing the handler.
- Register the tool in
src/server.ts. - Add MCP behavior tests and any domain integration tests.
- Replace the package name, server identity, image references, and README content.
Keep tool modules responsible for their own schemas and handlers. Keep transport modules independent from domain tools. Introduce services or persistence only when real behavior requires them.
Run the complete repository suite:
pnpm install --frozen-lockfile
pnpm format:check
pnpm lint
pnpm typecheck
pnpm test:unit
pnpm test:integration
pnpm buildFor container changes:
docker buildx build --load -t mcp-typescript-starter:test .Finally, connect an MCP client and confirm that echo is listed and returns both text
and structured content. In HTTP mode, confirm /healthz reports {"status":"ok"}.
- The example exposes one tool and no resources or prompts.
- Streamable HTTP has no authentication. Restrict it to loopback, a trusted LAN, a VPN, a private container network, or an authenticated reverse proxy.
- Host and Origin allowlists prevent classes of DNS rebinding attacks but do not authenticate callers.
- The HTTP server is stateless and contains no shared persistence or distributed coordination.
- Rate limiting, tracing, metrics, and domain-specific logging are not included.
- The repository is a source starter, not a published npm library.
Review SECURITY.md before exposing the HTTP transport or reporting a security issue.
Contributions are welcome. Before opening a pull request:
pnpm install --frozen-lockfile
pnpm format:check
pnpm lint
pnpm typecheck
pnpm test
pnpm build
docker buildx build --load -t mcp-typescript-starter:test .Changes must preserve strict typing, bounded validation, structured MCP results, stdout protocol purity, secure HTTP defaults, deterministic tests, and documentation for user-visible behavior. Do not add abstractions without a concrete use case for them.
MIT. See LICENSE.