npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mcp-server-framework

v1.2.0

Published

Production-ready MCP Server Framework with built-in logging, metrics, sessions, and multi-transport support.

Readme

MCP Server Framework

License: LGPL-3.0 Node.js TypeScript MCP SDK Express

A production-ready TypeScript framework for building Model Context Protocol servers — born out of the belief that MCP server development shouldn't mean reinventing infrastructure every time.

Why This Framework?

When I started building an MCP server for Komodo MCP Server, I quickly realized that the MCP SDK gives you protocol handling, but everything around it — transport management, session lifecycle, security middleware, configuration, observability, error handling — you're on your own. Every MCP server project ends up solving the same problems from scratch if you want a flexible, production-ready MCP server.

This framework extracts that infrastructure into a reusable foundation. The goal is simple: you focus on your API, tools, resources and prompts — the framework handles everything else.

It's opinionated where it matters (security defaults, structured logging, transport abstraction) and flexible where it should be (pluggable session stores, optional OpenTelemetry, multiple transport modes). Whether you're building a local CLI tool for Claude Desktop or a multi-client HTTP service in Docker — the same defineTool() call works everywhere.

Disclaimer

This framework is a personal project built with best effort and care. AI tools (GitHub Copilot, Claude) were used during development — for code generation, architecture exploration, and documentation. All AI-generated code and documentation has been critically reviewed, tested, and refined by me.

That said: this software is provided as-is, without warranty of any kind. See the license for the full legal terms. If you find bugs or have ideas, issues and contributions are always welcome.

License

LGPL-3.0 — GNU Lesser General Public License v3.0 or later

Features

🔌 Transport & Protocol

  • Multi-Transport — Stdio, Streamable HTTP (stateful & stateless), SSE (legacy). Same defineTool() code works everywhere.
  • HTTPS / TLS — Native TLS support with configurable cert/key paths, passphrase, and trust proxy for reverse proxies.
  • Stateless Mode — Per-request sessions for serverless and edge deployments, zero state overhead.
  • SSE Resumability — Pluggable EventStore interface for resilient SSE stream reconnections.

🔐 Authentication & Security

  • OAuth 2.1 / OIDC — Opt-in auth with three patterns: OIDC auto-discovery (Keycloak, Auth0, Okta, Azure AD), upstream OAuth proxy (GitHub, Google), and custom token verification.
  • Scope-Based Access Control — Permission guards (requireAuth, requireScope, hasScope) for fine-grained tool access.
  • Security Middleware Stack — Helmet headers, DNS rebinding protection, configurable rate limiting, protocol version validation — all enabled by default for HTTP transports.
  • Secret Scrubbing — JWT, Bearer tokens, and API keys automatically redacted from logs. CWE-117 log injection guard strips ANSI escapes and neutralizes injection attempts.
  • Tool-Result Redaction — every tool result passes a central, fail-closed secret-scrub boundary before it reaches the client transcript (default ON, configurable via scrubToolResults).

🛠️ Developer Experience

  • Type-Safe FactoriesdefineTool(), defineResource(), defineResourceTemplate(), definePrompt() with full Zod schema validation and auto-completion.
  • MCP AppsdefineApp() combines a tool with a UI resource in one declaration, enabling rich client interfaces via _meta.ui.resourceUri.
  • MCP Tasks (experimental)defineTask() for async background work with tasks/list, tasks/get, tasks/cancel protocol support.
  • Zero-Boilerplate — Auto-registration via global registries. Import a tool file = it's registered. createServer() picks it up automatically.
  • Response Helperstext(), json(), error(), image(), audio(), multi() — no manual content array assembly.
  • Builder PatternMcpServerBuilder fluent API for full control: custom providers, lifecycle hooks, explicit wiring.

📊 Observability & Operations

  • OpenTelemetry — Distributed tracing and metrics with zero-cost lazy loading (no overhead when disabled). OTLP and Prometheus export.
  • Structured Logging — Pipeline-based logger with JSON (ECS) and text formatters, file + console writers, AsyncLocalStorage for automatic request context.
  • Dual Logging — Framework logs go to stderr/files; MCP client notifications go to the connected client via SDK — simultaneously.
  • Health Endpoints — Kubernetes-ready /health (liveness) and /ready (readiness with API connectivity, session capacity, and configuration checks).

⚙️ Configuration & Session Management

  • 12-Factor Config — Five-level cascade: defaults → .env → config file (TOML/YAML/JSON) → environment variables → programmatic overrides.
  • Pluggable Session StoreSessionStore interface with in-memory default. Bring your own Redis, PostgreSQL, or any backend.
  • Session Lifecycle — Configurable idle timeouts, heartbeat keep-alive, dead connection cleanup, and max session capacity.
  • Graceful Shutdown — SIGINT/SIGTERM handling, session drain, configurable shutdown timeouts.

🧩 Error System

  • Typed Error HierarchyAppError base with categories: MCP, Session, Transport, Auth, Validation, Configuration, Operation, System.
  • FrameworkErrorFactory — Single import for all error types with unique IDs, HTTP/JSON-RPC code mappings, recovery hints, and cause chains.

Quick Start

import { createServer, defineTool, text, z } from 'mcp-server-framework';

defineTool({
  name: 'greet',
  description: 'Greet someone',
  input: z.object({ name: z.string() }),
  handler: async ({ input }) => text(`Hello, ${input.name}!`),
});

const { start } = createServer({
  name: 'my-server',
  version: '1.0.0',
});

await start();

That's it. The tool is auto-registered, the server starts on stdio by default.

Installation

npm install mcp-server-framework @modelcontextprotocol/sdk zod

Express, Zod, and the MCP SDK are regular dependencies, installed automatically — HTTP transport works out of the box (set MCP_TRANSPORT=http). The OpenTelemetry packages are optional dependencies: installed by default, but a stdio-only deployment can skip them with npm install --omit=optional to save ~15-20 MB. Telemetry degrades gracefully when absent.

For OpenTelemetry (all optional):

npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/sdk-metrics \
  @opentelemetry/instrumentation-http @opentelemetry/instrumentation-express \
  @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-prometheus

Requirements: Node.js ≥20.0.0, TypeScript ≥5.0

Primary API

High-Level: createServer() + define*()

import { createServer, defineTool, defineResource, definePrompt, text, json, z } from 'mcp-server-framework';

// Tools — auto-registered in the global registry
defineTool({
  name: 'health',
  description: 'Check server health',
  input: z.object({}),
  handler: async () => text('OK'),
});

// Resources — static URI-based content
defineResource({
  uri: 'config://version',
  name: 'Version Info',
  handler: async () => json({ version: '1.0.0' }),
});

// Prompts — reusable prompt templates
definePrompt({
  name: 'summarize',
  description: 'Summarize a topic',
  args: [{ name: 'topic', description: 'Topic to summarize', required: true }],
  handler: async ({ args }) => ({
    messages: [{ role: 'user', content: { type: 'text', text: `Summarize: ${args.topic}` } }],
  }),
});

// Start the server
const { start } = createServer({
  name: 'my-server',
  version: '1.0.0',
});
await start();

Advanced: McpServerBuilder

For full control over server composition:

import { McpServerBuilder } from 'mcp-server-framework';

const server = new McpServerBuilder()
  .withOptions({
    name: 'advanced-server',
    version: '1.0.0',
    transport: { mode: 'http', host: '0.0.0.0', port: 8000 },
  })
  .withToolProvider(myToolRegistry)
  .withResourceProvider(myResourceProvider)
  .withLifecycleHooks({
    onStarted: () => console.log('Server ready'),
  })
  .build();

await server.start();

Definition Helpers

| Function | Description | Auto-Registration | |----------|-------------|-------------------| | defineTool() | MCP Tool with Zod schema and handler | globalToolRegistry | | defineResource() | Static MCP Resource (URI-based) | globalResourceRegistry | | defineResourceTemplate() | URI-template Resource with parameters | globalResourceRegistry | | definePrompt() | MCP Prompt with optional arguments | globalPromptRegistry | | defineApp() | MCP App (Tool + Resource composition) | globalToolRegistry + globalResourceRegistry | | defineTask() | Async background task tool (experimental) | the task-tool registry (internal) |

Response Helpers

| Function | Description | |----------|-------------| | text() | Text content response | | json() | JSON-serialized response | | error() | Error response with isError: true | | image() | Base64-encoded image | | audio() | Base64-encoded audio | | multi() | Multi-content response |

Transport Modes

| Transport | Protocol | Session Mode | Use Case | |-----------|----------|--------------|----------| | Stdio | — | Single session | CLI, local development (Claude Desktop, VS Code) | | Streamable HTTP (stateful) | 2025-03-26 | Persistent sessions | Production, Docker, multi-client | | Streamable HTTP (stateless) | 2025-03-26 | Per-request | Serverless, edge, horizontal scaling | | SSE (legacy) | 2024-11-05 | Persistent sessions | Backwards compatibility |

// Stdio (default)
createServer({ transport: { mode: 'stdio' } });

// HTTP
createServer({ transport: { mode: 'http', host: '0.0.0.0', port: 8000 } });

// HTTPS
createServer({ transport: { mode: 'https', host: '0.0.0.0', port: 8443 } });

// Stateless (serverless / edge)
createServer({ transport: { mode: 'http', host: '0.0.0.0', port: 8000, stateless: true } });

Configuration

The framework follows the 12-Factor App methodology. Sources are merged in order of precedence:

Defaults → .env file → Config file → Environment variables → Programmatic overrides

| Variable | Default | Description | |----------|---------|-------------| | MCP_TRANSPORT | stdio | Transport mode: stdio, http, https | | MCP_BIND_HOST | 127.0.0.1 | Host to bind to | | MCP_PORT | 8000 | Port to listen on | | MCP_BASE_URL | (derived) | Public base URL behind a proxy/domain (OAuth issuer/redirects, RFC 9728 metadata URL, trusted host) | | MCP_STATELESS | false | Stateless mode (no sessions) | | MCP_JSON_RESPONSE | false | Prefer JSON over SSE for non-streaming responses | | MCP_BODY_SIZE_LIMIT | 1mb | Max request body size | | LOG_LEVEL | info | Log level: trace, debug, info, warn, error | | LOG_AUDIT_TOOL_IO | summary | Audit request/result depth per tool call (scrubbed): off | summary | full | | OTEL_ENABLED | false | Enable OpenTelemetry tracing & metrics |

Config file (auto-discovered as mcp-server.toml, .yaml, .json in CWD):

[transport]
mode = "http"
host = "0.0.0.0"
port = 8000

[security]
trust_proxy = "loopback"

[session]
timeout_ms = 1800000

[logging]
level = "info"
format = "json"

Subpath Exports

For advanced use cases, internal modules are available via subpath imports:

| Import Path | Purpose | |-------------|---------| | mcp-server-framework | Main API: createServer, defineTool, auth providers, guards, helpers | | mcp-server-framework/logger | Logger configuration, child loggers, SecretScrubber | | mcp-server-framework/session | Session internals, SessionStore interface | | mcp-server-framework/telemetry | OpenTelemetry tracing & metrics | | mcp-server-framework/errors | Error classes & FrameworkErrorFactory | | mcp-server-framework/http | Express app, HTTP/HTTPS server (lazy-loaded) | | mcp-server-framework/config | Config cache, env schema, Zod helpers |

import { FrameworkErrorFactory } from 'mcp-server-framework/errors';
import { withSpan } from 'mcp-server-framework/telemetry';
import { SecretScrubber } from 'mcp-server-framework/logger';
import type { SessionStore } from 'mcp-server-framework/session';

Architecture

src/
├── mcp/           # MCP Protocol Layer
│   ├── capabilities/  # Registries, define*() factories, apps, tasks
│   ├── handlers/      # Ping, progress
│   ├── responses/     # text(), json(), error(), image(), audio(), multi()
│   └── types/         # ToolDefinition, ResourceDefinition, ToolContext
├── server/        # Server Infrastructure
│   ├── auth/          # OAuth 2.1, OIDC auto-discovery, guards
│   ├── builder/       # McpServerBuilder (fluent API)
│   ├── http/          # Express app factory, HTTP/HTTPS server (lazy-loaded)
│   ├── middleware/     # Helmet, DNS rebinding, rate limiting, auth
│   ├── routes/        # Health, readiness, metrics, streamable HTTP, SSE
│   ├── session/       # SessionManager, SessionStore, Housekeeper, auth context
│   └── transport/     # Stdio, Streamable HTTP, SSE transports
├── telemetry/     # OpenTelemetry traces, metrics, SDK init (lazy-loaded)
├── logger/        # Structured Logging
│   ├── audit/         # Parallel JSON-Lines security/activity audit trail
│   ├── core/          # Pipeline, request context, injection guard (CWE-117)
│   ├── formatters/    # JSON (ECS), text
│   └── writers/       # Console, file, composite
├── errors/        # Error System
│   ├── core/          # AppError, ErrorCodes, HTTP/JSON-RPC mappings
│   └── categories/    # Validation, Protocol, Session, Transport, Auth, Operation, System
├── config/        # Configuration
│   └──                # Env schema (Zod), config cache, file parser
└── utils/         # Helpers — SecretScrubber engine, string/env/interpolation helpers

Security

The HTTP middleware stack runs before the MCP SDK processes any request:

Request → Trust Proxy → Helmet → DNS Rebinding → Rate Limiter → Auth → Protocol Version → SDK
  • Helmet: Security headers (CSP, X-Frame-Options, HSTS, Referrer-Policy, etc.)
  • DNS Rebinding Protection: Host header validation against allowed hosts
  • Rate Limiting: Configurable window, max requests, and custom key generators
  • Trust Proxy: Native Express trust-proxy support for reverse proxies (nginx, Traefik, Cloud LBs)
  • Secret Scrubbing: JWT, Bearer tokens, API keys automatically redacted from all log output
  • Log Injection Guard: CWE-117 protection — ANSI escapes stripped, injection attempts neutralized

Tool-Result Redaction

Every tool (and task-tool) result passes a single secret-redaction boundary between the handler and the SDK — before it reaches the client transcript and the model provider. Default ON.

createServer({
  // true (default) | false | { additionalKeys, allowKeys }
  scrubToolResults: {
    additionalKeys: ["fernet"], // extra key-name fragments to redact
    allowKeys: ["public_key"], // exact key names never redacted by key-matching
  },
});
  • structuredContent is scrubbed recursively; text content, embedded-resource text, and resource_link fields are scrubbed as text. Binary payloads (image/audio data, resource blob) pass through verbatim.
  • Domain rules are declared, not coded: dropKeys (remove a field entirely), sensitivePaths (mask innocuously-named keys in a secret context, e.g. endpoint.params.url), and maskWhenSibling (flag-driven masking, e.g. is_secretvalue). Values masked in structured content are purged from the same result's rendered text automatically.
  • Fail-closed: if scrubbing itself fails, the result is withheld and replaced by a generic error — an unscrubbed result never leaves the server.
  • Tools whose purpose is returning a secret opt out via defineTool({ scrubResult: false }).
  • resources/read bypasses this boundary entirely, so resources are scrubbed at two sibling stages instead: ephemeral resources are scrubbed on register in the DynamicResourceRegistry (same scrub option shape on configureDynamicResourceRegistry); static resources/templates are scrubbed on read (same scrubber as scrubToolResults, wired automatically).
  • Declare the real mimeType for structured content. Registry content whose mimeType contains json or toml is parsed, scrubbed as an object and re-serialized, so the declarative rules above apply with full structural fidelity. Everything else — including structured data labelled text/plain — gets regex redaction only, which cannot drop a nested field and which over-redacts allowlisted keys.
  • resolveScrubConfig(base?) layers the MCP_SCRUB_ENABLED / MCP_SCRUB_ADDITIONAL_KEYS / MCP_SCRUB_ALLOW_KEYS env vars onto a programmatic base (env wins on the master switch; key lists are unioned) — pass its result to both scrubToolResults and configureDynamicResourceRegistry's scrub option so the two stay in sync:
    const scrub = resolveScrubConfig({ dropKeys: ["deployed_config"] });
    createServer({ scrubToolResults: scrub });
    configureDynamicResourceRegistry({ scrub });
    It is opt-in: a consumer that passes its own config straight to createServer() never sees MCP_SCRUB_* take effect. Note also that base: false carries no rules, so re-enabling via MCP_SCRUB_ENABLED=true yields the generic heuristics without your domain rules — compose explicitly if yours must survive every switch combination.

Authentication

OAuth 2.1 and OIDC authentication is opt-in — servers work without auth by default. A single factory, createOAuthProvider(config | config[], options), builds the provider from a config discriminated on type: 'github' | 'google' | 'oidc' | 'oauth2'.

import { createServer, createOAuthProvider } from 'mcp-server-framework';

// OIDC auto-discovery (Keycloak, Auth0, Okta, Azure AD, ...)
const { provider, callbackHandler } = await createOAuthProvider(
  {
    type: 'oidc',
    issuer: 'https://auth.example.com',
    clientId: process.env.OIDC_CLIENT_ID!,
    clientSecret: process.env.OIDC_CLIENT_SECRET!,
  },
  { serverUrl: 'http://localhost:8000' },
);

const { start } = createServer({
  name: 'secure-server',
  version: '1.0.0',
  transport: { mode: 'http' },
  auth: { enabled: true, provider, callbackHandler },
});
await start();

Pass an array of configs to enable multi-provider mode — users get an auto-rendered provider-selection page when no ?provider= is present.

Supported patterns:

  • OIDC Auto-Discovery (type: 'oidc') — fetches .well-known/openid-configuration from the issuer automatically
  • Upstream OAuth (type: 'github' / 'google') — built-in presets; only clientId/clientSecret needed
  • Custom OAuth2 (type: 'oauth2') — the explicit-endpoints escape hatch for any other provider
  • Custom Token Verifier — implement TokenVerifier for API keys, custom JWTs, or any auth scheme
  • Permission GuardsrequireAuth(), requireScope(), hasScope() for tool-level access control

Health (/health) and metrics (/metrics) endpoints remain unauthenticated for liveness/readiness probes.

Error Handling

All framework errors extend AppError with structured metadata:

import { FrameworkErrorFactory } from 'mcp-server-framework/errors';

throw FrameworkErrorFactory.mcp.invalidRequest('Missing required parameter');
throw FrameworkErrorFactory.session.expired('sess-abc123');
throw FrameworkErrorFactory.validation.fieldRequired('name');

Each error includes: unique errorId (UUID), code, statusCode (HTTP), mcpCode (JSON-RPC), recoveryHint, cause chain, and timestamp.

Process Error Handling

The framework handles graceful shutdown (SIGINT, SIGTERM) but delegates process-level error handling to consumers. Add these handlers in your server entry point:

process.on('unhandledRejection', (reason) => {
  logger.error('Unhandled rejection: %s', reason);
});

process.on('uncaughtException', (error) => {
  logger.error('Uncaught exception: %s', error.message);
  process.exit(1);
});

Tech Stack

Core

| Technology | Version | Purpose | |------------|---------|---------| | TypeScript | ^5.0 | Strict mode, ES2022 target, exactOptionalPropertyTypes, verbatimModuleSyntax | | Node.js | ≥20.0.0 | Runtime (ESM, node: built-ins) | | @modelcontextprotocol/sdk | ^1.27.1 | MCP Protocol SDK — tools, resources, prompts, transports | | Express | ^5.2.1 | HTTP/HTTPS transport (lazy-loaded, see DD-018) | | Zod | ^3.25.0 | Runtime schema validation for tool inputs and config |

Security & Middleware

| Technology | Version | Purpose | |------------|---------|---------| | Helmet | ^8.1.0 | Security headers (CSP, X-Frame-Options, HSTS, etc.) | | express-rate-limit | ^8.2.1 | Configurable request rate limiting | | CORS | ^2.8.5 | Cross-origin resource sharing |

Observability (opt-in, lazy-loaded)

| Technology | Version | Purpose | |------------|---------|---------| | @opentelemetry/sdk-node | ^0.219.0 | OTEL SDK orchestration | | @opentelemetry/api | ^1.9.0 | Tracing API, span context | | @opentelemetry/sdk-metrics | ^2.6.0 | Metrics collection & export | | @opentelemetry/instrumentation-http | ^0.219.0 | Automatic HTTP span instrumentation | | @opentelemetry/instrumentation-express | ^0.61.0 | Automatic Express route instrumentation | | @opentelemetry/exporter-prometheus | ^0.219.0 | Prometheus /metrics endpoint | | @opentelemetry/exporter-trace-otlp-http | ^0.219.0 | OTLP trace export (Jaeger, Grafana, etc.) |

Configuration & Utilities

| Technology | Version | Purpose | |------------|---------|---------| | dotenv | ^17.3.1 | .env file loading | | smol-toml | ^1.3.1 | TOML config file parsing | | yaml | ^2.7.1 | YAML config file parsing |

Note: Express, Zod, and the MCP SDK are regular dependencies, installed automatically. The heavy OpenTelemetry packages (SDK, exporters, instrumentation) are optional dependencies — installed by default but safe to omit (npm install --omit=optional) for stdio-only deployments; telemetry degrades gracefully when absent. @opentelemetry/api (the lightweight tracing API re-exported from this package's public types) stays a regular dependency so type resolution never breaks.