mcp-typescript-starter
# MCP TypeScript Starter
[](https://www.typescriptlang.org/)
[](https://github.com/lukegskw/mcp-typescript-starter/actions/workflows/container.yml)
[](https://github.com/lukegskw/mcp-typescript-starter/pkgs/container/mcp-typescript-starter)
[](LICENSE)
**MCP TypeScript Starter** is a production-conscious foundation for building a
[Model Context Protocol](https://modelcontextprotocol.io/) server with TypeScript.
It includes one typed example tool, stdio and Streamable HTTP transports, strict
validation, tests, a hardened container, npm artifact verification, automated GHCR
publication, and opt-in npm/MCP Registry releases.
Clone it, replace the example domain, and keep the infrastructure that real MCP
servers need.
## Navigation
- [Use this starter](#use-this-starter)
- [About](#about)
- [Features](#features)
- [MCP tools](#mcp-tools)
- [Tech stack](#tech-stack)
- [Installation](#installation)
- [Configuration](#configuration)
- [MCP client setup](#mcp-client-setup)
- [Customizing the starter](#customizing-the-starter)
- [Distribution and releases](#distribution-and-releases)
- [Verification](#verification)
- [Limitations](#limitations)
- [Contributing](#contributing)
- [License](#license)
## Use this starter
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](#customizing-the-starter).
Fork this repository when you want to contribute improvements back through a
pull request. See [Contributing](#contributing) before submitting changes.
If this starter helped you, consider giving the repository a star. It helps
other TypeScript developers discover the project.
## About
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.
## Features
- Registers tools with strict Zod input and output schemas.
- Demonstrates server instructions and descriptions for every tool input and output.
- 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.
- Packs, installs, and starts the npm artifact in CI before it can be published.
- Publishes multi-architecture images only after quality checks pass.
- Provides an opt-in, resumable release workflow for npm, GHCR, and the MCP Registry.
- Validates release identity and metadata locally with the same checks used by CI.
- Provides an opt-in Gemini CLI extension manifest for gallery discovery.
## MCP tools
### `echo`
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:
```json
{
"message": "Hello, MCP!",
"metadata": {
"source": "example-client"
}
}
```
Example structured output:
```json
{
"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.
## Tech stack
- [Node.js 24+](https://nodejs.org/)
- [TypeScript](https://www.typescriptlang.org/) with strict project rules
- [Model Context Protocol TypeScript SDK 2](https://github.com/modelcontextprotocol/typescript-sdk)
- [Hono](https://hono.dev/)
- [Zod](https://zod.dev/)
- [Vitest](https://vitest.dev/)
- [pnpm](https://pnpm.io/)
- [Docker](https://www.docker.com/)
## Installation
### Prerequisites
- Node.js 24+ and pnpm 11 for local development.
- Docker and Docker Compose for container deployment.
### Docker Compose
The recommended HTTP deployment uses the published multi-architecture image:
```text
ghcr.io/lukegskw/mcp-typescript-starter:latest
```
Download the Compose example and provide the hostname clients will use:
```sh
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 -d
```
The Streamable HTTP and health endpoints will be available at:
```text
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
```sh
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:latest
```
### Build the container from source
```sh
git clone https://github.com/lukegskw/mcp-typescript-starter.git
cd mcp-typescript-starter
docker buildx build --load -t mcp-typescript-starter:local .
```
### Local Node.js installation
```sh
git clone https://github.com/lukegskw/mcp-typescript-starter.git
cd mcp-typescript-starter
pnpm install --frozen-lockfile
pnpm build
pnpm start -- --transport stdio
```
For local Streamable HTTP development:
```sh
MCP_TRANSPORT=streamable-http pnpm dev
```
## Configuration
| 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.
## MCP client setup
For a client that accepts Streamable HTTP server definitions:
```yaml
mcp_servers:
starter:
url: http://127.0.0.1:3000/mcp
```
For a client that launches a local stdio server:
```json
{
"mcpServers": {
"starter": {
"command": "node",
"args": [
"/absolute/path/to/mcp-typescript-starter/dist/main.js",
"--transport",
"stdio"
]
}
}
}
```
After publishing a project derived from this starter, clients can launch its pinned npm
package without cloning the repository:
```json
{
"mcpServers": {
"example": {
"command": "npx",
"args": ["--yes", "@example/example-mcp@0.1.0"]
}
}
}
```
When testing the published command from inside its own source checkout, some npm
versions prefer the root package. Use the local `node dist/main.js` configuration above
or set the MCP server's working directory outside the checkout.
Claude Code and Codex accept the equivalent CLI definitions:
```sh
claude mcp add --transport stdio example -- npx --yes @example/example-mcp@0.1.0
codex mcp add example -- npx --yes @example/example-mcp@0.1.0
```
Gemini CLI uses the same `mcpServers` JSON structure in its `settings.json`. Add any
domain credentials through each client's environment configuration and keep those files
private.
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.
## Customizing the starter
The main extension points are intentionally direct:
1. Copy or replace [`src/tools/echo.ts`](src/tools/echo.ts).
2. Define strict input and output schemas before writing the handler.
3. Register the tool in [`src/server.ts`](src/server.ts).
4. Add MCP behavior tests and any domain integration tests.
5. Replace the package name, executable name, server identity, image references,
repository metadata, and README content.
6. Keep `private: true` until the distribution checklist below is complete.
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.
Treat tool metadata as part of the public API. Describe every input and output field,
state prerequisites and side effects in each tool description, publish accurate safety
annotations, and add server instructions when callers need to understand a workflow
across multiple tools. The behavior test demonstrates how to inspect the definitions an
MCP client actually receives.
## Distribution and releases
The starter verifies its npm artifact on every pull request but cannot publish by
default. This prevents a newly generated repository from releasing under the starter's
identity.
To enable distribution in a derived project:
1. Update `name`, `bin`, `repository`, `homepage`, `bugs`, and `keywords` in
`package.json`. Add `mcpName` with the official reverse-DNS MCP name.
2. Copy `server.example.json` to `server.json`, then replace its name, repository, npm
identifier, OCI identifier, description, and environment variables. Use an exact OCI
version tag, not `latest`.
3. Update `SERVER_NAME` in `src/server.ts` and the default MCP server label in the
`Dockerfile`.
4. Run `pnpm test:package`, then remove `private: true` from `package.json` and run
`pnpm test:distribution`. The distribution check rejects stale versions, mismatched
package/Registry/container identities, and remaining starter placeholders.
5. Configure npm Trusted Publishing for `.github/workflows/publish.yml`, or add an
`NPM_TOKEN` repository secret as a fallback.
6. Create the repository variable `MCP_RELEASE_ENABLED` with the value `true` only when
all identities and registry permissions are ready.
### Optional Gemini CLI gallery distribution
To make a derived server installable as a Gemini CLI extension, copy and customize the
example manifest:
```sh
cp gemini-extension.example.json gemini-extension.json
```
Replace the extension name, description, MCP server key, npm package, and any settings.
Represent required credentials with environment substitutions such as
`${EXAMPLE_API_KEY}` and declare a matching `settings` entry; mark keys, passwords,
secrets, and tokens as `sensitive: true`. Add the `gemini-cli-extension` GitHub topic
after the customized manifest is committed.
The [Gemini gallery](https://geminicli.com/docs/extensions/releasing/) crawls public
tagged repositories that have that topic and a `gemini-extension.json` at the repository
root. The release preparation script updates the manifest version automatically when the
optional file exists. Projects that do not create it retain the same release behavior.
Prepare a release with one command:
```sh
pnpm release:prepare 0.1.0
```
This synchronizes the package version, Registry version, npm package version, OCI tag,
and optional Gemini extension version. Review and commit the result, run
`pnpm test:distribution`, then push it to `main`. The release workflow runs all quality
gates, validates the same distribution contract, creates `v0.1.0`, and publishes the
versioned container, npm package, MCP Registry entry, and GitHub release.
The regular container workflow owns `latest`, branch, pull-request, and SHA tags. The
release workflow exclusively owns immutable version tags and checks each external
artifact independently, so rerunning a partial release resumes the missing work.
See [the distribution design](docs/distribution-design.md) for the reliability and
ownership decisions behind this workflow.
## Verification
Run the complete repository suite:
```sh
pnpm install --frozen-lockfile
pnpm format:check
pnpm lint
pnpm typecheck
pnpm test:unit
pnpm test:integration
pnpm build
pnpm test:package
pnpm test:distribution
```
For container changes:
```sh
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"}`.
## Limitations
- 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](SECURITY.md) before exposing the HTTP transport or reporting a
security issue.
## Contributing
Contributions are welcome. Before opening a pull request:
```sh
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.
## License
MIT. See [LICENSE](LICENSE).
TDQS
Scored across 1 tool
With only one tool, there is no possibility of confusion or overlap. The single 'echo' tool is clearly distinct by virtue of being the only tool available.
The single tool name 'echo' is short, clear, and directly describes its function. Since there is only one tool, naming consistency is trivially satisfied.
One tool feels minimal even for a starter server, but it is a common pattern for a simple template. The count is at the lower edge of being acceptable rather than egregiously insufficient.
The server only provides an echo function, which is useful for testing connectivity but lacks any real domain operations. There are no CRUD or workflow capabilities, representing a significant gap for any practical use beyond a basic demo.