Production-oriented Model Context Protocol server scaffolding for TypeScript. This repository gives you a working starting point for building MCP servers that need more than a bare transport layer. It already includes transport bootstrapping, tool registration, schema validation, execution timeouts, context resolution, plugin hooks, logging, authentication hooks, and a test harness.
The goal of this project is not only to expose tools to an MCP client, but to do it in a way that remains understandable when the server grows. A real MCP server usually has to answer three questions at once: what tools exist, what inputs are valid, and what context should be injected before those tools run. This scaffold separates those concerns so new tools can be added without reworking the runtime.
Important
This repository is a scaffold, not a finished product. The runtime, transports, schemas, and tool registry are real, but the service clients are intentionally lightweight examples that you are expected to adapt to your own systems and trust boundaries.
- What This Project Is
- Why This Scaffold Exists
- Feature Snapshot
- Tech Stack
- Architecture
- Repository Layout
- How The Runtime Works
- Getting Started
- Configuration
- Included Tools
- Extension Model
- Testing
- Operational Guidance
This repository is a TypeScript MCP server that can run over either Streamable HTTP or stdio. The runtime is created in src/server.ts, started from src/index.ts, and configured through environment variables read by src/config/serverConfig.ts. The server builds a set of tool definitions, wraps them in a registry, validates inputs and outputs with JSON schema, and then exposes them to an MCP client through the official @modelcontextprotocol/sdk.
In practice, that means you can use this repository as a base when you need a server that supports structured tool inputs, context-aware execution, integration stubs, and predictable operational behavior. It is useful when you want to avoid writing the same transport, auth, validation, and logging code for every new MCP service.
Small MCP demos are easy to write, but they often become difficult to maintain once you introduce multiple tools, external services, and different execution environments. This scaffold exists to solve that exact transition. It separates the transport layer from the tool layer, keeps schemas explicit, and gives each request a consistent execution context so that authentication, mission state, and logging can be handled in one place.
That structure matters because MCP servers usually sit between language models and operational systems. When a model is allowed to call a tool, you need to know what the tool does, how the arguments are validated, how long it is allowed to run, and how errors are surfaced back to the client. Those concerns are already wired here.
Note
The scaffold defaults to HTTP transport when no environment override is present. The integration tests also assert that behavior, which makes the default visible in both the code and the test suite.
| # | Area | What It Does | Why It Matters |
|---|---|---|---|
| 1 | Transport modes | Runs over Streamable HTTP or stdio using the official MCP SDK. | Lets the same server support local development flows and remotely hosted clients. |
| 2 | Tool registry | Registers tool definitions once and executes them through a shared registry. | Keeps validation, plugins, and timeout handling consistent across every tool. |
| 3 | Schema validation | Validates tool inputs and optional outputs against JSON schema. | Prevents malformed requests from reaching tool logic and improves client interoperability. |
| 4 | Context providers | Builds system, user, and mission context for each execution. | Gives tools the runtime facts they need without hard-coding environment state into every handler. |
| 5 | Auth hooks | Supports API-key checks for HTTP requests and scope-aware execution metadata. | Creates a clear place to replace development auth with production identity controls. |
| 6 | Plugin lifecycle | Offers request start, success, and error hooks around tool execution. | Makes auditing, metrics, and policy enforcement easier to add later. |
| 7 | Timeout wrapper | Stops long-running tool execution after a configured limit. | Protects the server from stuck dependencies and preserves client responsiveness. |
| 8 | Testing | Includes Vitest coverage for context, tool registration, and integration behavior. | Gives you a starting verification layer before you add domain-specific code. |
Note: This table summarizes the runtime capabilities that already exist in the repository today, not aspirational roadmap items.
The stack is intentionally narrow. Each dependency supports a specific part of the server lifecycle, and there is very little framework overhead beyond the MCP SDK itself.
| # | Dependency | Role In This Repository | Why It Was Chosen |
|---|---|---|---|
| 1 | TypeScript | Implements the runtime, tool contracts, and context types. | Provides type safety across request handling, service wiring, and tool execution. |
| 2 | @modelcontextprotocol/sdk | Supplies the MCP server primitives, stdio transport, and Streamable HTTP transport. | It is the canonical SDK for implementing standards-aligned MCP servers. |
| 3 | Express | Hosts the HTTP transport endpoints and health route. | It keeps the HTTP side simple and readable without hiding request flow. |
| 4 | Ajv | Validates JSON schemas for tool input and output envelopes. | It is fast, mature, and well suited for schema-first interfaces. |
| 5 | Zod | Defines runtime input shapes used when registering tools with the MCP SDK. | It keeps the tool registration surface ergonomic while JSON schema remains authoritative for validation. |
| 6 | dotenv | Loads environment variables during startup. | It makes local configuration predictable without custom bootstrapping code. |
| 7 | Vitest | Runs unit and integration tests. | It is fast enough for iterative work and straightforward for TypeScript projects. |
| 8 | tsx | Runs TypeScript directly during development. | It shortens the edit-run loop and avoids a manual build step while iterating. |
Note: This table explains the current production and development dependencies declared in package.json and how they map to the repository structure.
The runtime is centered around a single composition path. Configuration is loaded first, service clients are created from that configuration, tool definitions are built from those services, and a ToolRegistry executes them behind a uniform validation and plugin boundary. The transport layer sits outside that registry so HTTP and stdio can share the same execution model.
flowchart TD
A[Client] --> B{Transport}
B -->|HTTP| C[Express + Streamable HTTP]
B -->|stdio| D[Stdio Transport]
C --> E[createMcpInstance]
D --> E
E --> F[ToolRegistry]
F --> G[Schema Validation]
G --> H[Plugin Hooks]
H --> I[Execution Context]
I --> J[Tool Handler]
J --> K[Service Clients]
J --> L[Structured Result]
Note: This diagram shows the execution path from an MCP client to a tool result, including the layers that enforce validation and shared runtime behavior.
The most important architectural decision in this codebase is that tool code does not own transport setup, authentication parsing, or context assembly. Those concerns are handled in the runtime before a tool executes. That keeps tool handlers focused on domain work instead of protocol plumbing.
Tip
If you plan to add many tools, keep following the current pattern: define schema first, create a thin tool wrapper second, and isolate remote API or database logic inside src/services/. That separation will scale much better than embedding service calls directly inside the transport layer.
The directory structure is organized around responsibilities rather than technology layers alone. Tools live separately from services because tools define the contract that clients see, while services handle the implementation details of talking to other systems. Context providers are also isolated because context is cross-cutting and should not be rebuilt independently inside every handler.
| # | Path | Purpose | Why It Exists |
|---|---|---|---|
| 1 | src/index.ts | Starts the runtime and exits cleanly on fatal startup errors. | Keeps process entry concerns separate from server composition. |
| 2 | src/server.ts | Builds services, registry, transports, and runtime startup flow. | Acts as the core composition root for the application. |
| 3 | src/config/ | Loads and normalizes environment-driven server configuration. | Prevents configuration parsing from leaking into business logic. |
| 4 | src/context/ | Resolves system, user, and mission context objects. | Gives tool execution a consistent context envelope. |
| 5 | src/tools/ | Defines MCP-visible tools and their input shapes. | Creates a stable contract for model-facing interactions. |
| 6 | src/services/ | Implements integration-facing client logic and logging. | Separates external system access from tool orchestration. |
| 7 | src/plugins/ | Provides lifecycle extension points around execution. | Lets you add observability or policy hooks without changing every tool. |
| 8 | src/schemas/ | Stores JSON schema documents for tool requests and shared types. | Keeps tool contracts explicit and reusable. |
| 9 | src/utils/ | Holds auth, validation, timeout, and error helpers. | Centralizes infrastructure behavior that multiple slices depend on. |
| 10 | tests/ | Verifies context behavior, tool registration, and integration defaults. | Documents expected behavior while protecting against regressions. |
Note: This table is a high-level guide to where responsibilities live so new contributors can add features without guessing where code belongs.
Expanded folder tree
mcp-server/
├── package.json
├── README.md
├── tsconfig.json
├── src/
│ ├── index.ts
│ ├── server.ts
│ ├── config/
│ │ └── serverConfig.ts
│ ├── context/
│ │ ├── missionContext.ts
│ │ ├── systemContext.ts
│ │ └── userContext.ts
│ ├── plugins/
│ │ └── index.ts
│ ├── schemas/
│ │ ├── assuranceSchema.json
│ │ ├── commonTypes.json
│ │ ├── customToolSchema.json
│ │ ├── jiraSchema.json
│ │ └── searchSchema.json
│ ├── services/
│ │ ├── dbClient.ts
│ │ ├── jiraClient.ts
│ │ ├── loggingService.ts
│ │ └── nasaAssuranceClient.ts
│ ├── tools/
│ │ ├── customTool.ts
│ │ ├── index.ts
│ │ ├── jiraTool.ts
│ │ ├── nasaAssuranceTool.ts
│ │ └── searchTool.ts
│ ├── types/
│ │ ├── ContextTypes.ts
│ │ └── ToolTypes.ts
│ └── utils/
│ ├── auth.ts
│ ├── errorHandler.ts
│ └── schemaValidator.ts
└── tests/
├── contextTests.test.ts
├── integration.test.ts
└── toolTests.test.ts
At startup, createRuntime() loads normalized configuration, creates service clients, loads built-in plugins, and builds the tool registry. From there, the runtime selects HTTP or stdio mode based on MCP_TRANSPORT. The result is one composition path with two transport adapters instead of two different servers that happen to expose similar tools.
When a request reaches a tool, the runtime authenticates it, resolves context, validates the input schema, runs plugin hooks, executes the tool with a timeout, optionally validates the output schema, and formats the result for the MCP client. This is the core reason the scaffold is useful: every tool benefits from the same safety and observability envelope.
sequenceDiagram
participant Client
participant Transport
participant Runtime
participant Registry
participant Tool
participant Service
Client->>Transport: MCP request
Transport->>Runtime: Forward request
Runtime->>Runtime: Authenticate and build execution context
Runtime->>Registry: Execute tool(name, args, context)
Registry->>Registry: Validate input schema
Registry->>Tool: Run handler with timeout
Tool->>Service: Query dependency or perform action
Service-->>Tool: Return data
Tool-->>Registry: Structured result
Registry->>Registry: Validate output schema when configured
Registry-->>Client: Summary + structuredContent
Note: This sequence diagram shows the control flow that every tool invocation shares, regardless of which transport receives the request.
| # | Runtime Stage | What Happens | Operational Benefit |
|---|---|---|---|
| 1 | Configuration load | Environment variables are parsed into a typed config object. | Startup behavior stays deterministic across environments. |
| 2 | Service creation | Logger and integration clients are constructed once. | Prevents scattered initialization logic. |
| 3 | Tool definition assembly | Tools are created from shared services. | Encourages consistent dependency injection. |
| 4 | Request authentication | Headers and scopes are evaluated before execution. | Rejects unauthorized traffic early. |
| 5 | Context resolution | System, user, and mission facts are injected into the execution context. | Gives tools richer information without manual plumbing. |
| 6 | Validation and timeout | Input is validated and execution is bounded by time. | Improves safety and resilience under bad inputs or slow dependencies. |
| 7 | Lifecycle hooks | Plugins observe success and failure paths. | Supports auditing and metrics without rewriting handlers. |
| 8 | Structured response | The MCP client receives human-readable text and machine-readable content. | Helps both interactive use and downstream automation. |
Note: This table breaks the runtime into concrete stages so it is easier to reason about where to extend behavior and where to debug failures.
You can run the server locally in either HTTP mode or stdio mode. HTTP is the default and is useful when you want a network-accessible MCP endpoint. stdio is useful when another process spawns this server directly and communicates over standard input and output.
npm install
npm run dev:httpThat starts the server in HTTP mode using tsx for rapid iteration. The HTTP runtime exposes a health endpoint and the MCP endpoint handled by the official Streamable HTTP transport.
If you want the server to behave like a local subprocess tool host instead, run:
npm run dev:stdioOnce you are ready for a production-style build artifact, compile and run the emitted JavaScript:
npm run build
npm startTip
Use HTTP mode during integration testing with remote clients and stdio mode when validating that a desktop client or local orchestrator can spawn the process directly.
Configuration is environment-driven, and src/config/serverConfig.ts is the single normalization point. That design is important because environment variables are messy in raw form. The config layer turns string values into booleans, numbers, sets, and transport enums before the rest of the application reads them.
Create a local environment file or export variables in your shell before starting the server. The defaults are intentionally development-friendly, especially for transport mode and network binding.
MCP_TRANSPORT=http
MCP_HOST=127.0.0.1
MCP_PORT=8080
MCP_EXECUTION_TIMEOUT_MS=15000
MCP_LOG_LEVEL=info| # | Variable | Meaning | Default Behavior |
|---|---|---|---|
| 1 | MCP_SERVER_NAME | Logical server name advertised to clients. | Falls back to mcp-server. |
| 2 | MCP_SERVER_VERSION | Logical server version exposed by the runtime. | Falls back to 0.1.0. |
| 3 | MCP_TRANSPORT | Selects http or stdio mode. |
Defaults to HTTP unless explicitly set to stdio. |
| 4 | MCP_HOST | HTTP bind address. | Defaults to 127.0.0.1. |
| 5 | MCP_PORT | HTTP bind port. | Defaults to 8080. |
| 6 | MCP_CORS_ORIGINS | Controls allowed browser origins for HTTP mode. | * means any origin is allowed. |
| 7 | MCP_EXECUTION_TIMEOUT_MS | Caps tool execution time in milliseconds. | Defaults to 15000. |
| 8 | MCP_LOG_LEVEL | Sets the logging verbosity. | Defaults to info. |
Note: This table covers server-level configuration that changes transport and operational behavior.
| # | Variable | Meaning | Default Behavior |
|---|---|---|---|
| 1 | MCP_AUTH_ENABLED | Turns HTTP API-key checks on or off. | Defaults to disabled. |
| 2 | MCP_API_KEY_HEADER | Defines the HTTP header used for API-key lookup. | Defaults to x-mcp-api-key. |
| 3 | MCP_API_KEYS | Comma-separated list of valid API keys. | Parses into an empty set when unset. |
| 4 | MISSION_ID | Mission identifier injected into context. | Defaults to mission-sample. |
| 5 | MISSION_NAME | Mission display name injected into context. | Defaults to Mission Control Scaffold. |
| 6 | MISSION_ENVIRONMENT | Mission environment value exposed to tools. | Falls back to NODE_ENV or development. |
| 7 | JIRA_BASE_URL / JIRA_TOKEN | Jira integration connection settings. | Remain undefined until you wire a real service. |
| 8 | NASA_ASSURANCE_BASE_URL / NASA_ASSURANCE_TOKEN | NASA assurance integration connection settings. | Remain undefined until you wire a real service. |
Note: This table covers authentication, mission context, and external integration wiring.
Warning
The included API-key pattern is suitable as a development hook or for tightly controlled internal use, but it should not be treated as the final security model for public or internet-facing deployments. Replace it with a stronger authorization flow before exposing this server broadly.
The scaffold ships with four example tools so the registry, schemas, summaries, and service boundaries are visible immediately. Three are read-oriented examples and one is a write-capable placeholder. Together they demonstrate the difference between query-style tools and side-effecting actions.
| # | Tool Name | What It Does | Scope Pattern |
|---|---|---|---|
| 1 | jira_query | Searches Jira issues by query text with optional project and limit filters. | Requires tools:read. |
| 2 | nasa_assurance_lookup | Retrieves requirement assurance status and optional evidence details. | Requires tools:read. |
| 3 | search_knowledge | Searches indexed operational or assurance knowledge. | Requires tools:read. |
| 4 | custom_action | Acknowledges a domain-specific action payload and serves as the write example. | Requires tools:write. |
Note: This table maps directly to the tool names asserted by the integration tests, so it is a reliable snapshot of the tools currently registered by default.
{
"query": "thermal constraint",
"projectKey": "OPS",
"limit": 5
}The important detail is not the sample data itself but the contract shape. Each tool has an input schema identifier, an MCP-facing input shape, a handler, and a summary function. That combination gives the client a usable interface while keeping validation and execution behavior explicit.
The project is designed so that new capabilities are added as slices instead of as scattered edits. A new tool should normally involve a schema, a tool module, and optionally a service client. A new context provider should only change the context layer and runtime composition. A new plugin should hook into lifecycle events without changing the registry execution semantics.
| # | Extension Type | Where To Change Code | Typical Reason |
|---|---|---|---|
| 1 | New tool | src/schemas/, src/tools/, and src/tools/index.ts |
Add a new MCP capability visible to clients. |
| 2 | New service | src/services/ and createServices() in src/server.ts |
Connect tools to a new API, database, or internal system. |
| 3 | New context provider | src/context/ and resolveContexts() in src/server.ts |
Inject more runtime state into tool execution. |
| 4 | New plugin | src/plugins/ and createRuntime() wiring |
Add metrics, auditing, tracing, or policy checks. |
| 5 | New schema type | src/schemas/commonTypes.json or a tool-specific schema file |
Reuse structured contracts across multiple tools. |
Note: This table explains how to extend the scaffold without collapsing its separation between contracts, execution, and integrations.
Suggested workflow for adding a new tool
- Add or update the JSON schema in
src/schemas/so the contract is explicit first. - Create a tool module in
src/tools/that declares name, title, scopes, input shape, and handler. - If the tool talks to an external system, place that logic in
src/services/instead of the tool file. - Export the tool from
src/tools/index.tsso the registry can include it. - Add or update tests in
tests/so the registration and behavior remain visible.
The test suite is intentionally small but useful. It verifies that the runtime comes up with the expected tool list and that HTTP remains the default transport unless configuration overrides it. That baseline matters because documentation and operational expectations often drift first around entrypoints and defaults.
npm test| # | Test File | What It Covers | Why It Matters |
|---|---|---|---|
| 1 | tests/toolTests.test.ts | Tool-oriented behavior. | Keeps tool contracts and execution slices from regressing silently. |
| 2 | tests/contextTests.test.ts | Context provider behavior. | Protects the runtime facts injected into tools. |
| 3 | tests/integration.test.ts | Registry composition and default transport behavior. | Confirms the assembled runtime still matches its documented entry behavior. |
Note: This table highlights the current testing slices so contributors know where to extend coverage when they add new runtime behavior.
When you move from scaffold to production use, focus first on integration realism and trust boundaries. The code already gives you a stable structure for validation, request handling, and lifecycle hooks, but production readiness depends on the service implementations, the authentication model, and the operational policies you add around them.
Keep the runtime contract narrow and explicit. Tool names should be stable, schemas should be versioned with care, and logs should remain structured enough to support debugging and audit needs. The registry pattern in this repository makes that discipline easier because it creates one place to enforce it.
Note
A good production evolution path is usually: replace mock-like service behavior with real integrations, tighten auth, add tracing or audit plugins, and then expand the test suite around the new failure modes introduced by those external dependencies.
| # | Script | Command | Use Case |
|---|---|---|---|
| 1 | build | tsc -p tsconfig.json |
Compile the project for a production-style run. |
| 2 | dev | tsx watch src/index.ts |
General development entrypoint. |
| 3 | dev:http | tsx watch src/index.ts |
Explicit HTTP development mode. |
| 4 | dev:stdio | cross-env MCP_TRANSPORT=stdio tsx watch src/index.ts |
stdio development mode for spawned clients. |
| 5 | start | node dist/index.js |
Run the built output. |
| 6 | test | vitest run |
Execute the test suite once. |
| 7 | test:watch | vitest |
Run tests in watch mode during local development. |
Note: This table is derived from package.json and should stay synchronized with the actual scripts in the repository.
This scaffold is most valuable when you preserve its boundaries. Let the runtime own transport and execution policy, let tools own client-facing capability definitions, and let services own external integration details. That architecture is what keeps an MCP server understandable after it grows beyond a single demo tool.
If you keep that separation intact, this repository can scale from a simple local prototype to a more serious internal tool platform without forcing a full rewrite of the server core.