Skip to main content
Glama
aself101

MCP Security Framework

by aself101

MCP Security Framework

npm version License: MIT Node.js TypeScript

A secure-by-default MCP server built on the official SDK with 5-layer validation. Provides defense-in-depth against traditional attacks and AI-driven threats.

This framework implements defense-in-depth security with zero configuration required, protecting MCP servers from path traversal, command injection, SQL injection, XSS, prototype pollution, SSRF, and 20+ additional attack vectors.

Quick Start

Installation

npm install mcp-secure-server

HTTP transport status (0.1.0-security): experimental until 0.2.0. It serves many clients, one MCP session each (through 0.0.27 it answered only the first request per process), but it has no authentication and Layer 3's budget is shared by every client of a server — see HTTP Transport. Upgrading HTTP users: clients must send the Mcp-Session-Id the server issues (SDK clients do), browser clients must be listed in allowedOrigins, and registrations freeze when the handler is attached.

Upgrading to 0.0.26-security behind a reverse proxy, load balancer or CDN: per-client-IP limits over HTTP no longer trust X-Forwarded-For or X-Real-IP unless you configure trustProxy (the old behaviour let any client choose its own rate-limit key). Until you set it, every client behind the proxy shares one budget — e.g. 60 GET/DELETE per minute for all users together. If the proxy's own address is private (RFC 1918, ULA, loopback, link-local), Node prints a one-time MCP_SECURE_SERVER_TRUST_PROXY warning; a proxy with a public address gets no warning. Set trustProxy to your proxies' exact addresses before upgrading: see Client IP and trustProxy. Servers exposed directly need no change. The opt-in Layer 5 rate limit now keys on the client address over HTTP instead of the Mcp-Session-Id header.

Upgrading to 0.0.23-security: three security fixes that can start rejecting calls a server accepted before. A tool's maxArgsSize is now enforced whether or not the tool declares argsShape (it was silently skipped without one); tool-call arguments that are not a plain object are refused; and every byte-denominated size limit (maxMessageSize, maxParamBytes, suspiciousMessageSize, maxArgsSize, maxEgressBytes) now counts UTF-8 bytes rather than characters, so non-ASCII payloads near a limit measure up to 3x larger. Review your caps before upgrading. See CHANGELOG.md.

Basic Usage

import { SecureMcpServer } from 'mcp-secure-server';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

// Create secure server with a security preset
const server = new SecureMcpServer(
  { name: 'my-server', version: '1.0.0' },
  {
    securityLevel: 'standard',  // 'basic' | 'standard' | 'paranoid' | 'custom'
    // Layer 4 denies any tools/call whose name is not registered here (fail
    // closed). server.registerTool() below does NOT register with Layer 4 by itself.
    toolRegistry: [{ name: 'calculator', sideEffects: 'none' }]
  }
);

// Register tools exactly like McpServer — registerTool(), the MCP SDK's current API
server.registerTool('calculator', {
  description: 'Basic calculator',
  inputSchema: { left: z.number(), right: z.number() }
}, async ({ left, right }) => {
  return { content: [{ type: 'text', text: `Result: ${left + right}` }] };
});

// Connect - transport is automatically wrapped with security
const transport = new StdioServerTransport();
await server.connect(transport);

Security Presets

Choose your security level with a single option:

// Development: relaxed limits, minimal validation
const devServer = new SecureMcpServer(
  { name: 'dev', version: '1.0.0' },
  { securityLevel: 'basic' }
);

// Production: balanced security (default)
const prodServer = new SecureMcpServer(
  { name: 'prod', version: '1.0.0' },
  { securityLevel: 'standard' }
);

// High-security: maximum protection
const secureServer = new SecureMcpServer(
  { name: 'secure', version: '1.0.0' },
  { securityLevel: 'paranoid' }
);

// Custom: override specific values within a preset
const customServer = new SecureMcpServer(
  { name: 'custom', version: '1.0.0' },
  {
    securityLevel: 'standard',
    maxRequestsPerMinute: 60  // Override just this value
  }
);

Preset

Use Case

Message Size

Rate Limit

Burst

Automation Detection

basic

Development, testing

100KB

120/min

30/10s

Disabled

standard

Production (default)

50KB

30/min

10/10s

Enabled

paranoid

High-risk, compliance

25KB

15/min

5/5s

Enabled (strict)

custom

Full control

You decide

You decide

You decide

You decide

Programmatic Preset Access

Access preset configurations programmatically for dynamic configuration, validation, or custom tooling:

import {
  SECURITY_PRESETS,
  resolvePreset,
  getDefaultPreset,
  isValidPreset
} from 'mcp-secure-server';

// Get the default preset name
const defaultName = getDefaultPreset();  // 'standard'

// Validate user input
const userInput = 'paranoid';
if (isValidPreset(userInput)) {
  const config = resolvePreset(userInput);
  console.log(config.maxMessageSize);      // 25600
  console.log(config.maxRequestsPerMinute); // 15
}

// Iterate all presets for documentation or UI
for (const [name, config] of Object.entries(SECURITY_PRESETS)) {
  console.log(`${name}: ${config.maxRequestsPerMinute} req/min`);
}

// Build dynamic configuration
function createServer(env: string) {
  const level = env === 'production' ? 'paranoid' : 'basic';
  return new SecureMcpServer(
    { name: 'dynamic', version: '1.0.0' },
    { securityLevel: level }
  );
}

With Logging (Opt-in)

const server = new SecureMcpServer(
  { name: 'my-server', version: '1.0.0' },
  {
    securityLevel: 'standard',
    enableLogging: true,
    verboseLogging: true,
    logPerformanceMetrics: true,
    logLevel: 'debug'
  }
);

Full TypeScript support with exported types for all parameters, configurations, and responses.

Related MCP server: pc-control-mcp

Table of Contents

Cookbook Examples

Example MCP servers demonstrating the security framework. Each server includes input validation and attack prevention.

Server

Description

Tools

Auth

advanced-validation-server

Layer 5 custom validators (PII detection, geofencing, business hours, egress tracking)

Financial query, batch process, export data, API call

None

api-wrapper-server

Safe REST API wrapping with domain restrictions and rate limiting

Weather, currency conversion, news headlines

None

cli-wrapper-server

Safe CLI tool wrapping with command injection prevention

Git status, image resize, PDF metadata, video encode

None

database-server

Secure database operations with SQL injection prevention

User queries, order creation, report generation

None

filesystem-server

Protected file system access with path traversal prevention

Read files, list directories, search files

None

http-server

Simple HTTP transport with createHttpServer()

Calculator, echo

None

http-session-server

Several HTTP clients at once, one MCP session each: per-session logging and sampling, browser CORS, the registration freeze

Notes, slow task, summarize (sampling), session info, log-to-me

None

image-gen-server

Unified image generation across 5 providers (BFL, Google, Ideogram, OpenAI, Stability)

Generate, edit, upscale, describe images

API keys

kenpom-server

College basketball analytics and efficiency ratings

Ratings, schedules, scouting reports, player stats

KenPom login

monitoring-server

Observability with metrics, audit logging, and alerts

Security metrics, audit log, alerts, Prometheus export

None

multi-endpoint-server

Multiple HTTP endpoints with createSecureHttpHandler()

Admin (list-users, system-stats), Public (health, status)

None

nba-server

NBA stats, live scores, and player data

Player stats, box scores, live scoreboard

None

transaction-server

Method chaining enforcement for secure transaction workflows

Session, accounts, prepare/confirm/execute transactions

None

See the cookbook README for setup instructions and detailed documentation.

Overview

The MCP Security Framework acts as a universal wrapper for any MCP server, providing comprehensive security validation through a multi-layered architecture. It implements:

  • 5-Layer Defense Pipeline - Structure, Content, Behavior, Semantics, and Contextual validation

  • Zero Configuration - Security enabled by default with sensible defaults

  • Universal Compatibility - Works with any MCP server using @modelcontextprotocol/sdk

  • Extensible Layer 5 - Add custom validators, domain restrictions, OAuth validation

  • Tested - 1264 tests with 93.76% line coverage, including a dist/ smoke suite that runs the published artifact in a separate process

  • Opt-in File Logging - the audit logger is off by default; blocked requests always emit one [SECURITY] line to stderr (never stdout)

  • Performance Optimized - Content caching and efficient pattern detection

  • Full TypeScript Support - Complete type definitions with strict mode

Architecture

Request → Layer 1 → Layer 2 → Layer 3 → Layer 4 → Layer 5 → MCP Server
           │          │          │          │          │
        Structure  Content   Behavior  Semantics  Contextual
        Validation Validation Validation Validation Validation

Visual Overview

                          MCP Security Framework (5 Layers by Default)
                                          │
    ┌─────────────┬─────────────┬─────────────┬─────────────┬─────────────┐
    │             │             │             │             │             │
┌───▼────┐  ┌─────▼─────┐  ┌────▼────┐  ┌────▼─────┐  ┌─────▼──────┐
│ Layer 1│  │  Layer 2  │  │ Layer 3 │  │  Layer 4 │  │  Layer 5   │
│ Struct.│  │  Content  │  │ Behavior│  │ Semantics│  │ Contextual │
└────────┘  └───────────┘  └─────────┘  └──────────┘  └────────────┘
│JSON-RPC│  │Injection  │  │Rate     │  │Tool      │  │Custom      │
│Format  │  │Detection  │  │Limiting │  │Contracts │  │Validators  │
│Size    │  │XSS/SQLi   │  │Burst    │  │Quotas    │  │Domain/OAuth│
│Encoding│  │Path Trav. │  │Patterns │  │Policies  │  │Response Val│
└────────┘  └───────────┘  └─────────┘  └──────────┘  └────────────┘

Security Layers

Layer 1 - Structure Validation

Validates the fundamental structure of incoming JSON-RPC messages.

Protections:

  • JSON-RPC 2.0 format validation

  • Request size limits (default: 50KB)

  • Message encoding validation

  • Parameter count limits

  • Per-string parameter length limits (default: 5,000 chars)

  • Method name length limits

Configuration:

{
  maxMessageSize: 50000,      // Maximum message size in bytes
  maxParamCount: 100,         // Maximum recursive parameter count (set to Infinity to disable)
  maxStringLength: 5000       // Maximum length of any single string parameter value (chars)
}

Method name length is capped at 100 characters. This limit is not configurable — MCP method names are protocol-level identifiers (tools/call, resources/read, …) and never legitimately approach it.

Note: maxMessageSize must leave headroom above maxStringLength — the message envelope is larger than the string it carries, and the message-size check fires first.

Layer 2 - Content Validation

Detects and blocks malicious content patterns in request parameters.

Protections: Path traversal, command injection, SQL/NoSQL injection, XSS, prototype pollution, XML entity attacks (XXE), CRLF injection, SSRF, CSV injection, LOLBins, GraphQL introspection, deserialization attacks, JNDI/Log4Shell, buffer overflow patterns, and more.

See SECURITY.md for the complete list of 200+ attack patterns with examples.

Configuration:

{
  maxParamBytes: 50000,       // Max serialized params payload (bytes)
  contentValidation: {
    enabled: true,
    debugMode: false          // Enable for detailed pattern match info
  }
}

Note — the size caps stack. A large payload faces four independent ceilings, and the first one to fire names itself in the rejection: maxMessageSize (Layer 1 envelope), maxStringLength (Layer 1, per string), maxParamBytes (Layer 2, serialized params), and suspiciousMessageSize (Layer 3, hard block). Tools that legitimately accept large structured payloads (bulk saves, document stores) need all four raised together — raising only one moves the rejection to the next ceiling in the stack. The per-tool maxArgsSize (Layer 4) is a fifth, tool-scoped ceiling that cannot admit what the global caps reject.

Note — shell-access patterns require invocation context. The command.shellAccess patterns run at every security level (ALWAYS_CHECK), including STORAGE tools with relaxedFields. As of 0.0.19-security they match shell invocations (powershell -enc …, cmd /c …, non-shebang /bin/sh), not bare mentions — stored prose like "PowerShell users" or a #!/bin/sh shebang no longer trips them. Known limitation: stored content quoting a complete shell invocation (e.g. a security report's reverse-shell example) is still rejected at STORAGE level. This is deliberate; if it becomes a problem for your workload, the escape hatch is field-level exclusion via relaxedFields, whose content is not pattern-scanned at all.

Layer 3 - Behavior Validation

Rate limiting and request pattern analysis to prevent abuse.

Protections:

  • Requests per minute rate limiting

  • Requests per hour rate limiting

  • Burst detection (configurable time window)

  • Automation detection via timing analysis

  • Large message flagging

Configuration:

{
  maxRequestsPerMinute: 30,   // Rate limit per minute
  maxRequestsPerHour: 500,    // Rate limit per hour
  burstThreshold: 10,         // Max requests in burst window
  burstWindowMs: 10000,       // Burst detection window in ms (default: 10s)
  suspiciousMessageSize: 20000, // BLOCK messages larger than this (bytes) — a hard
                                // rejection, not a log-only flag; keep aligned with
                                // maxMessageSize/maxParamBytes for large-payload tools
  automationDetection: {
    enabled: true,            // Enable timing-based automation detection
    sampleSize: 5,            // Number of requests to analyze
    maxVariance: 50,          // Max timing variance (ms) before flagging
    minInterval: 100,         // Min avg interval (ms) to flag as automation
    maxInterval: 2000         // Max avg interval (ms) to flag as automation
  }
}

Automation Detection: Analyzes request timing patterns to detect automated scripts. When enabled, it monitors the variance in request intervals - suspiciously consistent timing (low variance) indicates automation rather than human interaction.

Layer 4 - Semantic Validation

Tool contract enforcement and resource access policies.

Protections:

  • Tool argument validation against schemas

  • Response size limits (egress control)

  • Per-tool quota enforcement

  • Side effect declarations

  • Filesystem access control via resource policies

  • Session management

  • Method chaining enforcement (opt-in)

Configuration:

{
  toolRegistry: [
    {
      name: 'my-database-tool',
      sideEffects: 'write',       // 'none' | 'read' | 'write' | 'network'
      maxArgsSize: 5000,          // Max argument size in bytes
      maxEgressBytes: 100000,     // Max response size
      quotaPerMinute: 30,
      quotaPerHour: 500,
      argsShape: {                // Expected argument schema
        query: { type: 'string' },
        limit: { type: 'number' }
      }
    }
  ],
  resourcePolicy: {
    allowedSchemes: ['file'],
    rootDirs: ['./data', './public'],
    denyGlobs: ['/etc/**', '**/*.key', '**/.env'],
    maxPathLength: 4096,
    maxReadBytes: 2000000         // 2MB max file read
  },
  maxSessions: 5000,
  sessionTtlMs: 1800000            // 30 minutes
}

Method Chaining Enforcement

Layer 4 can enforce valid method call sequences to prevent abuse patterns like calling dangerous tools without proper initialization.

Enable chaining enforcement:

{
  enforceChaining: true,           // Enable method chaining (default: false)
  chainingDefaultAction: 'deny',   // 'allow' | 'deny' when no rule matches
  chainingRules: [
    // Allow any method to call initialize
    { from: '*', to: 'initialize' },
    // After initialize, can list tools, resources or resource templates
    { from: 'initialize', to: 'tools/list' },
    { from: 'initialize', to: 'resources/list' },
    { from: 'initialize', to: 'resources/templates/list' },
    // After listing tools, can call them
    { from: 'tools/list', to: 'tools/call' },
    // Tool-to-tool calls allowed
    { from: 'tools/call', to: 'tools/call' },
  ]
}

ChainingRule type:

interface ChainingRule {
  from: string;              // Method to transition from ('*' for any)
  to: string;                // Method to transition to ('*' for any)
  fromTool?: string;         // Tool name glob pattern (e.g., 'file-*', '*-http*')
  toTool?: string;           // Tool name glob pattern
  fromSideEffect?: SideEffectType;  // 'none' | 'read' | 'write' | 'network'
  toSideEffect?: SideEffectType;
  action?: 'allow' | 'deny'; // Default: 'allow'
  id?: string;               // Rule identifier for logging (named in denial messages)
}

ChainingRule and SideEffectType are exported from mcp-secure-server.

Default method allowlist. Independently of chaining, Layer 4 refuses any method not in its allowlist with INVALID_MCP_METHOD. The defaults are initialize, ping, tools/list, tools/call, resources/list, resources/read, resources/templates/list (since 0.0.23-security — before that every ResourceTemplate a server registered was undiscoverable), prompts/list, prompts/get, and the notifications/initialized, notifications/cancelled and notifications/progress notifications. The default chaining rules include initialize → resources/templates/list and resources/templates/list → resources/read. Add or remove methods with the methodSpec option (type MethodSpecOverride), which merges per method over these defaults.

Advanced example - block dangerous transitions:

{
  enforceChaining: true,
  chainingDefaultAction: 'allow',  // Allow by default
  chainingRules: [
    // Block read tools from calling write tools directly
    {
      from: 'tools/call',
      to: 'tools/call',
      fromSideEffect: 'read',
      toSideEffect: 'write',
      action: 'deny',
      id: 'no-read-to-write'
    },
    // Block file-* tools from calling *-http* tools
    {
      from: 'tools/call',
      to: 'tools/call',
      fromTool: 'file-*',
      toTool: '*-http*',
      action: 'deny',
      id: 'no-file-to-http'
    }
  ]
}

Rules are evaluated first-match-wins. Tool patterns use simple glob matching (* = any chars, ? = single char).

Layer 5 - Contextual Validation

Custom validators, domain restrictions, and response filtering. Fully extensible.

Protections:

  • Custom validator registration with priorities

  • Domain blocklist/allowlist enforcement

  • OAuth URL validation

  • Response content validation (PII detection, etc.)

  • Cross-request state via context store

  • Global rules that run before validators

Configuration:

{
  contextual: {
    enabled: true,                // Set false to disable Layer 5
    domainRestrictions: {
      enabled: true,
      blockedDomains: ['evil.com', 'malicious.net'],
      allowedDomains: []          // Empty = allow all except blocked
    },
    oauthValidation: {
      enabled: true,
      allowedDomains: ['oauth.example.com'],
      blockDangerousSchemes: true
    },
    rateLimiting: {
      enabled: true,
      limit: 20,
      windowMs: 60000
    },
    maxContextEntries: 10000      // Context-store cap; expired swept, then oldest evicted
  }
}

Installation

From npm

# Install in your project (it is a library; there is no CLI to install globally)
npm install mcp-secure-server

The examples are TypeScript. Run one with npx tsx server.ts, or compile with tsc; plain node server.ts works only on Node versions that strip types natively (22.6+), not on the Node 18 and 20 this package supports.

From Source

# Clone the repository
git clone https://github.com/aself101/mcp-secure-server.git
cd mcp-secure-server

# Install dependencies
npm install

# Build TypeScript
npm run build

Dependencies (regular, installed with the package — not peer dependencies):

  • @modelcontextprotocol/sdk - MCP SDK (the server you wrap is built on the copy installed here)

  • zod - Schema validation

  • minimatch - Glob matching for tool-policy names

  • winston - The optional audit logger's transports

TypeScript Support

This package is written in TypeScript with strict mode enabled (noUncheckedIndexedAccess, strictNullChecks). All exports include complete type definitions.

SecureMcpServer's registration methods — tool, registerTool, resource, registerResource, prompt, registerPrompt — carry the underlying McpServer's own types (since 0.0.24-security; they were (...args: unknown[]) => unknown before), so handler arguments are inferred from your Zod schema exactly as with McpServer, and a handler must return a valid tool result.

Prefer registerTool() / registerResource() / registerPrompt(). The MCP SDK marks tool(), resource() and prompt() @deprecated in favour of the register* forms, and because these methods now carry the SDK's own types, that deprecation reaches your editor and linters (@typescript-eslint/no-deprecated). Both forms work and both are secured identically — registerTool() handlers get the same Layer 5 response wrapping as tool() handlers. Some examples below and in the cookbook still use tool().

Use the same zod the MCP SDK resolves. The SDK's tool typings accept Zod 3 and Zod 4 schemas through a compatibility layer; when your project's zod is a different copy from the one the SDK's types see (for example Zod 3.x in your app beside Zod 4.x under the SDK), TypeScript compares the two type trees structurally and tsc can take minutes and exhaust memory. This is SDK behaviour — plain McpServer does the same (measured: 55 s / 4.4 GB, out of memory, on a cookbook server; 0.3 s / 330 MB with one copy). Check with npm ls zod; align to a single Zod 4 version.

Exported Types

import {
  // Main classes
  SecureMcpServer,
  SecureTransport,
  ContextualValidationLayer,
  ContextualConfigBuilder,

  // Factory functions
  createContextualLayer,

  // Type guards
  isSeverity,
  isViolationType,
  isError,
  getErrorMessage,

  // Types
  SecurityOptions,
  ValidationResult,
  Severity,
  ViolationType,
  ToolSpec,
  ResourcePolicy,
  ValidationContext
} from 'mcp-secure-server';

Type-Safe Configuration

import { SecureMcpServer } from 'mcp-secure-server';
import type { SecureMcpServerOptions } from 'mcp-secure-server';

const options: SecureMcpServerOptions = {
  securityLevel: 'standard',
  maxMessageSize: 50000,
  maxParamCount: 100,           // Recursive key count limit (Infinity to disable)
  maxStringLength: 5000,        // Per-string parameter length limit (chars)
  maxParamBytes: 50000,         // Serialized params payload limit (bytes, Layer 2)
  maxRequestsPerMinute: 30,
  enableLogging: true,
  contextual: {
    enabled: true,
    domainRestrictions: {
      enabled: true,
      blockedDomains: ['evil.com']
    }
  }
};

const server = new SecureMcpServer(
  { name: 'my-server', version: '1.0.0' },
  options  // TypeScript validates all options
);

SecureMcpServerOptions is the constructor's actual parameter type and always carries the full option set. The older SecurityOptions name is now a deprecated alias of it — it was previously a separate interface that drifted out of sync with the real options.

Validation Results

interface ValidationResult {
  passed: boolean;
  allowed: boolean;               // alias of passed (backward compatibility)
  severity: Severity;             // 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
  reason: string | null;          // sanitized failure reason
  violationType: ViolationType | string | null;  // 'PATH_TRAVERSAL' | 'SQL_INJECTION' | ...
  confidence: number;             // 0.0 - 1.0
  timestamp: number;
  layerName: string | null;
  detectionLayer?: string;
  validatorSource?: string;
  validationTime?: number;
}

All non-suffixed fields are required on the interface. In plain JavaScript, custom Layer 5 validators may return partial literals like { passed: true } — the framework normalizes them at runtime. In TypeScript, annotate custom validators loosely (or return the full shape); a literal { passed: true } does not satisfy ValidationResult.

Building from Source

# Install dependencies
npm install

# Build TypeScript to JavaScript
npm run build

# Output is in dist/
ls dist/
# index.js, index.d.ts, security/*.js, security/*.d.ts, types/*.d.ts

Configuration

Full Configuration Reference

const server = new SecureMcpServer(
  { name: 'my-server', version: '1.0.0' },
  {
    // ═══════════════════════════════════════════
    // Layer 1 & 2 - Structure & Content Validation
    // ═══════════════════════════════════════════
    maxMessageSize: 50000,        // Max message size (bytes)
    maxParamCount: 100,           // Max recursive parameters (Infinity to disable)
    maxStringLength: 5000,        // Max length of any single string parameter (chars)
    maxParamBytes: 50000,         // Max serialized params payload (bytes, Layer 2)
    // (method name length is fixed at 100 chars — not configurable)

    // ═══════════════════════════════════════════
    // Layer 2 - Content Validation
    // ═══════════════════════════════════════════
    // Enabled by default with all pattern detection

    // ═══════════════════════════════════════════
    // Layer 3 - Behavior Validation
    // ═══════════════════════════════════════════
    maxRequestsPerMinute: 30,     // Rate limit per minute
    maxRequestsPerHour: 500,      // Rate limit per hour
    burstThreshold: 10,           // Max requests in burst window
    burstWindowMs: 10000,         // Burst window duration (ms)
    suspiciousMessageSize: 20000, // Block large messages (bytes) — hard rejection
    automationDetection: {        // Timing-based automation detection
      enabled: true,              // Enable/disable detection
      sampleSize: 5,              // Requests to analyze
      maxVariance: 50,            // Max timing variance (ms)
      minInterval: 100,           // Min interval to flag (ms)
      maxInterval: 2000           // Max interval to flag (ms)
    },

    // ═══════════════════════════════════════════
    // Layer 4 - Semantic Validation
    // ═══════════════════════════════════════════
    toolRegistry: [               // Tool constraints
      {
        name: 'my-tool',
        sideEffects: 'write',
        maxArgsSize: 5000,
        maxEgressBytes: 100000,
        quotaPerMinute: 30
      }
    ],
    resourcePolicy: {             // Filesystem access control
      allowedSchemes: ['file'],
      rootDirs: ['./data'],
      denyGlobs: ['/etc/**', '**/*.key'],
      maxReadBytes: 2000000
    },
    maxSessions: 5000,
    sessionTtlMs: 1800000,
    methodSpec: {                 // Method allowlist — merges per method over the defaults
      shape: {
        'completion/complete': { required: [] },  // add a method the defaults refuse
        'prompts/get': null,                      // null removes a default method
      }
    },
    enforceChaining: false,       // Enable method chaining (default: false)
    chainingDefaultAction: 'deny', // 'allow' | 'deny' when no rule matches
    chainingRules: [              // Method transition rules
      { from: '*', to: 'initialize' },
      { from: 'initialize', to: 'tools/list' },
      { from: 'tools/list', to: 'tools/call' },
      { from: 'tools/call', to: 'tools/call' },
      // Advanced: tool patterns and side effects
      // { from: 'tools/call', to: 'tools/call', fromTool: 'read-*', toTool: 'write-*', action: 'deny' }
    ],

    // ═══════════════════════════════════════════
    // Layer 5 - Contextual Validation
    // ═══════════════════════════════════════════
    contextual: {
      enabled: true,              // false to disable Layer 5
      domainRestrictions: {
        enabled: true,
        blockedDomains: ['evil.com'],
        allowedDomains: []        // Empty = allow all except blocked
      },
      oauthValidation: {
        enabled: true,
        allowedDomains: ['oauth.example.com'],
        blockDangerousSchemes: true
      },
      rateLimiting: {
        enabled: true,
        limit: 20,
        windowMs: 60000
      }
    },

    // ═══════════════════════════════════════════
    // Error responses
    // ═══════════════════════════════════════════
    enableDetailedErrors: false,  // Include redacted violation reason in top-level
                                  // error.message (detail is always in data.reason)

    // ═══════════════════════════════════════════
    // Logging (file/audit logger disabled by default; blocked requests still
    // emit one [SECURITY] line to stderr — see ErrorSanitizerOptions.enableSecurityLogging)
    // ═══════════════════════════════════════════
    enableLogging: false,         // Enable security logging
    verboseLogging: false,        // Detailed decision logs
    logPerformanceMetrics: false, // Timing statistics
    logLevel: 'info',             // 'debug' | 'info' | 'warn' | 'error'
    logDir: './logs',             // Log directory; also settable via LOG_DIR env

    // ═══════════════════════════════════════════
    // Warnings (see HTTP Transport → Warnings)
    // ═══════════════════════════════════════════
    onWarning: (message, code) => console.warn(`[${code}] ${message}`), // default: process.emitWarning
    silenceWarnings: []           // codes to drop, each named; there is no "all"
  }
);

Log Directory Resolution

The log directory is resolved in this order:

  1. logDir option passed to the constructor

  2. LOG_DIR environment variable

  3. <cwd>/logs (default)

A relative value is resolved against the current working directory. Set logDir explicitly when the process may run with a non-writable working directory — for example, MCP hosts (such as Claude Desktop) that launch servers with cwd="/", where the default would resolve to the unwritable /logs.

If the resolved directory cannot be created or written, the logger degrades to no-op logging rather than crashing the host (logging must never crash the application). When this happens it emits a one-time warning to stderr, and getSecurityStats().logger.fileLoggingAvailable reports false (with writeErrors/lastWriteError for late failures) so the degraded state is observable rather than silent.

Tool Policies Configuration

Tool policies allow you to define security levels for individual MCP tools, enabling context-aware content validation. Tools that store documentation can have relaxed pattern detection, while tools that execute commands use full validation.

Security Levels

Level

Description

Use Case

EXECUTION

Full validation - all attack patterns checked

Command execution, file writes, system operations

QUERY

Standard validation - SQL/NoSQL patterns added

Database queries, API calls, search operations

STORAGE

Relaxed validation - critical patterns only

Issue trackers, notes, documentation storage

DISPLAY

Minimal validation - XSS and deserialization only

Read-only queries, help output, status displays

Configuration File

Create a tool-policies.json file in your project root or specify a path via the TOOL_POLICIES_PATH environment variable.

File resolution order:

  1. TOOL_POLICIES_PATH environment variable

  2. ./tool-policies.json (current working directory)

  3. ~/.config/mcp-secure-server/tool-policies.json (user config)

Basic Example

{
  "version": "2.0",
  "tools": {
    "save_note": {
      "level": "STORAGE",
      "relaxedFields": ["content", "title"],
      "description": "Stores user notes"
    },
    "get_status": {
      "level": "DISPLAY",
      "description": "Read-only status query"
    },
    "execute_command": {
      "level": "EXECUTION",
      "description": "Runs shell commands"
    }
  },
  "defaultLevel": "EXECUTION"
}

Full Schema

{
  "version": "2.0",

  "basePolicies": {
    "storage-content": {
      "level": "STORAGE",
      "relaxedFields": ["content", "description", "title"],
      "description": "Base policy for content storage tools"
    },
    "display-readonly": {
      "level": "DISPLAY",
      "description": "Base policy for read-only tools"
    }
  },

  "patterns": [
    { "match": "get_*", "policy": "display-readonly" },
    { "match": "list_*", "policy": "display-readonly" },
    { "match": "search_*", "policy": "display-readonly" },
    { "match": "{save,create,update}_*", "policy": "storage-content" }
  ],

  "tools": {
    "execute_sql": {
      "level": "QUERY",
      "description": "Database queries with SQL injection checks"
    },
    "save_document": "storage-content",
    "dangerous_operation": {
      "level": "EXECUTION",
      "description": "High-risk operation requiring full validation"
    }
  },

  "defaultLevel": "EXECUTION"
}

Schema Reference

Field

Type

Description

version

"2.0"

Required. Schema version (must be "2.0")

basePolicies

object

Optional. Reusable policy definitions

patterns

array

Optional. Glob patterns for tool matching

tools

object

Optional. Explicit tool policy definitions

defaultLevel

string

Optional. Fallback level for unknown tools

Policy Object

{
  level: 'EXECUTION' | 'QUERY' | 'STORAGE' | 'DISPLAY',  // Security level
  relaxedFields?: string[],  // Fields with relaxed validation
  skipPatterns?: string[],   // Pattern categories to skip
  description?: string,      // Documentation
  extends?: string           // Inherit from base policy
}

Pattern Matching

Patterns use minimatch glob syntax:

Pattern

Matches

get_*

get_users, get_status, get_config

*_issues

query_issues, search_issues, list_issues

{get,list}_*

get_users, list_users, get_config, list_items

file-*

file-read, file-write, file-delete

v?_tool

v1_tool, v2_tool

Resolution order:

  1. Explicit tool definitions (highest priority)

  2. Pattern matching (first match wins)

  3. defaultLevel from config

  4. EXECUTION level (secure default)

Inheritance with extends

Policies can inherit from base policies and add/override fields:

{
  "version": "2.0",
  "basePolicies": {
    "base-storage": {
      "level": "STORAGE",
      "relaxedFields": ["content"]
    }
  },
  "tools": {
    "save_note": {
      "extends": "base-storage",
      "relaxedFields": ["extra_field"],
      "description": "Inherits STORAGE level, merges relaxedFields"
    }
  }
}

The result for save_note:

  • level: STORAGE (inherited)

  • relaxedFields: ["content", "extra_field"] (merged and deduplicated)

Relaxed Fields

The relaxedFields array specifies parameter names that should use STORAGE-level validation regardless of the tool's overall level. Useful for tools that have both sensitive and content parameters:

{
  "tools": {
    "create_issue": {
      "level": "QUERY",
      "relaxedFields": ["description", "title"],
      "description": "QUERY level for project/priority, STORAGE for text content"
    }
  }
}

Runtime Registration

You can also register tool policies programmatically:

import { registerToolPolicy } from 'mcp-secure-server';

registerToolPolicy('my_custom_tool', {
  level: 'STORAGE',
  relaxedFields: ['content', 'notes'],
  description: 'Custom documentation tool'
});

Loading Configuration Programmatically

import { initializeToolPolicies, resetToolPolicies } from 'mcp-secure-server';

// Load from object
initializeToolPolicies({
  version: '2.0',
  patterns: [
    { match: 'get_*', policy: { level: 'DISPLAY' } }
  ],
  defaultLevel: 'QUERY'
});

// Reset to defaults
resetToolPolicies();

Validation Behavior by Level

Level

Checks SQL/NoSQL

Checks Command Injection

Checks Path Traversal

Checks XSS

EXECUTION

✅ All

✅ All (6 sub-categories)

✅

✅ All

QUERY

✅ All

⚠️ Critical only (shellAccess, executionWrappers)

✅

✅ All

STORAGE

❌

⚠️ Critical only (shellAccess, executionWrappers)

❌

✅ Critical

DISPLAY

❌

⚠️ Critical only (shellAccess, executionWrappers)

❌

✅ Critical

All levels check critical sub-categories (shell access, execution wrappers, XSS basic/advanced vectors, deserialization). EXECUTION_ONLY sub-categories (systemInfo, fileOperations, networkOperations, basicInjection) run only at EXECUTION level to avoid false positives on common words like "top", "curl", "grep" in documentation content.

Advanced Tool Policy Helpers

Additional utilities for programmatic tool policy management:

import {
  getToolsByLevel,
  isRelaxedField,
  isValidSecurityLevel,
  defaultToolPolicies
} from 'mcp-secure-server';

// Get all tools configured at a specific security level
const storageLevelTools = getToolsByLevel('STORAGE');
// Returns: ['save_note', 'create_document', ...]

// Check if a field has relaxed validation for a tool
if (isRelaxedField('save_note', 'content')) {
  // 'content' field uses STORAGE-level validation
}

// Validate security level strings
if (isValidSecurityLevel(userInput)) {
  // userInput is 'EXECUTION' | 'QUERY' | 'STORAGE' | 'DISPLAY'
}

// Access default policies (useful for extending)
const defaults = defaultToolPolicies;

Use cases:

  • Auditing: List all tools at each security level

  • Debugging: Check if a specific field is relaxed

  • Dynamic configuration: Validate user-provided security levels

  • Testing: Access defaults for baseline comparison

API Reference

SecureMcpServer

Drop-in replacement for McpServer with built-in 5-layer security.

import { SecureMcpServer } from 'mcp-secure-server';

const server = new SecureMcpServer(serverInfo, options);

MCP SDK Passthrough Methods

SecureMcpServer delegates to the underlying McpServer for most operations. Some methods add security enhancements.

Enhanced Methods (security added):

Method

Enhancement

tool()

Response validation via Layer 5, under the tool's current name (also after a rename); the handler receives every SDK argument

registerTool()

Response validation via Layer 5, as tool()

connect()

Wraps transport with SecureTransport

sendLoggingMessage(params, sessionId?)

With an HTTP session's id, reaches that session only; without an id, the stdio connection; an id that names no session is dropped

getClientCapabilities(sessionId?) / getClientVersion(sessionId?)

With an HTTP session's id, that session's client; otherwise the stdio client

Every registration method (tool, registerTool, resource, registerResource, prompt, registerPrompt), registerCapabilities(), and every call through a returned handle is recorded so HTTP sessions can replay it, and throws LateRegistrationError (with method and registrationName) once an HTTP handler is attached to the server — stdio use of the same server object included. Register everything first; see Registrations freeze when a handler is attached.

"stdio" in this README means the connection made through connect() — usually a StdioServerTransport, since HTTP goes through createSecureHttpHandler and the server factories instead. A transport you pass to connect() yourself, an SDK HTTP transport included, is that connection: it gets SecureTransport's validation and every "stdio" statement here, and none of the HTTP handler's sessions, Origin checks or bounds.

Passthrough Methods (delegation to McpServer):

// Resource and prompt registration
server.resource(name, uri, handler);
server.prompt(name, description, handler);

// Connection management
await server.close();
server.isConnected();

// Notification methods (MCP SDK passthrough)
server.sendResourceListChanged();
server.sendToolListChanged();
server.sendPromptListChanged();

Example with all registration types:

const server = new SecureMcpServer({ name: 'demo', version: '1.0.0' }, {
  toolRegistry: [{ name: 'add', sideEffects: 'none' }] // Layer 4 refuses tools not listed here
});

// Tool registration (response validation enabled)
server.tool('add', 'Add numbers', { a: z.number(), b: z.number() },
  async ({ a, b }) => ({ content: [{ type: 'text', text: `${a + b}` }] })
);

// Resource registration (passthrough)
server.resource('config', 'config://app', async () => ({
  contents: [{ uri: 'config://app', text: JSON.stringify(config) }]
}));

// Prompt registration (passthrough)
server.prompt('greeting', 'Generate greeting', async () => ({
  messages: [{ role: 'user', content: { type: 'text', text: 'Hello!' } }]
}));

await server.connect(new StdioServerTransport());

Security Methods

// Get security statistics
const stats = server.getSecurityStats();
// {
//   server: { uptime, totalLayers, enabledLayers, loggingEnabled },
//   behaviorLayer?: { ...request/burst counters },
//   logger?: { ...logging health, e.g. fileLoggingAvailable }  // only when logging enabled
// }

// Get detailed security report (requires enableLogging: true)
const report = server.getVerboseSecurityReport();

// Generate full report to file (requires enableLogging: true)
await server.generateSecurityReport();

// Graceful shutdown with final report
await server.shutdown();

Property Accessors

server.mcpServer;           // Access underlying McpServer
server.server;              // Access underlying Server
server.validationPipeline;  // Access validation pipeline

SecureTransport

Low-level transport wrapper for custom implementations.

import { SecureTransport } from 'mcp-secure-server';

const secureTransport = new SecureTransport(
  transport,       // Original transport
  validator,       // Validation function
  {
    errorSanitizer // Optional error sanitizer
  }
);

HTTP Transport

Experimental in 0.1.0. The transport serves many clients, one MCP session each, but it has no authentication, session ids are bearer credentials, and Layer 3's rate limits are shared by every client of a server. It is not for untrusted multi-tenant use until 0.2.0, which adds per-client budgets; the label is removed then, not before. Removing it will not add authentication: the exposure warning (below) carries that on its own and stays. Each process that creates a handler is reminded once at runtime (MCP_SECURE_SERVER_HTTP_EXPERIMENTAL, see Warnings).

Through 0.0.27 the transport answered only the first request per process: it shared one stateless SDK transport, which the SDK (1.26 and later) refuses to reuse, so every later request got 500 until restart. Every HTTP test sent one request to a fresh server, so none could see it.

For remote MCP servers, use the built-in HTTP transport with security validation. Zero external dependencies - uses node:http directly.

import { SecureMcpServer } from 'mcp-secure-server';
import { z } from 'zod';

const server = new SecureMcpServer(
  { name: 'my-server', version: '1.0.0' },
  {
    enableLogging: true,
    // Layer 4 refuses any tools/call whose name is not listed here (fail closed).
    toolRegistry: [{ name: 'add', sideEffects: 'none' }]
  }
);

// Register everything first: creating the HTTP server freezes registrations.
server.tool('add', 'Add two numbers', {
  a: z.number(),
  b: z.number()
}, async ({ a, b }) => ({
  content: [{ type: 'text', text: `${a + b}` }]
}));

const httpServer = server.createHttpServer({ endpoint: '/mcp' });
httpServer.listen(3000, '127.0.0.1', () => {
  console.log('MCP server listening on http://127.0.0.1:3000/mcp');
});

Bind 127.0.0.1 unless something in front of the server authenticates clients. Anything that can reach the port can open a session and call every tool. A handler whose listener is bound beyond loopback (0.0.0.0 included) warns once with MCP_SECURE_SERVER_HTTP_EXPOSED. In a container, where the server must bind 0.0.0.0 for the port to be published, publish it to the host's loopback (docker run -p 127.0.0.1:3000:3000 …) and silence the code by name.

The http-session-server cookbook example runs several clients at once: per-session logging and sampling, a browser client, the registration freeze, and a request log. It has been driven by Claude Code, the MCP Inspector and SDK clients.

Configuration options:

interface HttpServerOptions {
  endpoint?: string;                     // MCP endpoint path (default: '/mcp'; server factories only)
  maxBodySize?: number;                  // Max body size in bytes (default: 51200 = 50KB)
  requestTimeout?: number;               // Body parse timeout in ms (default: 30000)
  allowedOrigins?: string[];             // Browser origins allowed (default: [] — every browser origin refused)
  allowedHosts?: string[];               // Exact Host values accepted (default: no Host check; [] throws)
  maxHttpSessions?: number;              // Sessions per handler (default: 1000)
  maxHttpSessionsPerClient?: number;     // Sessions per client key (default: 10)
  httpSessionIdleTtlMs?: number;         // Close a session idle this long (default: 1800000 = 30 min)
  sessionlessRequestsPerMinute?: number; // Per-client GET/DELETE on known sessions (default: 60)
  sessionlessRequestsPerHour?: number;   // Per-client GET/DELETE on known sessions (default: 600)
  trustProxy?: number | string[];        // Proxies to trust for X-Forwarded-For (default: 0 = none)
}

The HTTP session options are not Layer 4's: maxSessions and sessionTtlMs (Layer 4) govern the sweep of the method-chaining state Layer 4 keeps per session — not the number of MCP sessions, and not quotas, which Layer 4 keeps per tool for the whole server. The sessionlessRequests* options meter GET and DELETE per client; the name predates sessions.

Sessions

A client starts a session with initialize; the response carries a server-issued id (crypto.randomUUID()) in Mcp-Session-Id, and every later request must send it. Each session has its own McpServer and SDK transport, built from the server's registrations.

Request

Answer

initialize POST, no header

a new session (subject to the bounds below)

any other request without Mcp-Session-Id

400 before the pipeline; charged to the error lockout unless it is a well-formed JSON-RPC POST (clients probe before initializing — Claude Code sends server/discover)

a session id the server did not issue, or has closed

404, always — never a 429; 404s are not throttled

POST with a known id

the 5-layer pipeline, then that session's server

GET with a known id

that session's SSE stream (one at a time; a second gets 409), closed after 30 minutes — the session survives

DELETE with a known id

ends that session

a JSON-RPC batch

400 (-32600); batches were removed from MCP in 2025-06-18

A session id is a bearer credential: whoever holds it is that session. The package logs only a hash of it; do the same in your validators, tool handlers and request logs, which see the raw id (extra.sessionId, extra.requestInfo.headers['mcp-session-id']):

import { createHash } from 'node:crypto';
const logId = (id: string) => createHash('sha256').update(id).digest('hex').slice(0, 12); // the package's form

The pipeline's context.sessionId is the server-issued id, so Layer 4 chaining state is per session. Notifications (notifications/initialized, cancelled, progress, …) are neither checked against chaining rules nor recorded, over stdio too: chaining governs request sequences, and a cancellation must always arrive.

Bounds (per handler):

Bound

Default

When reached

Sessions

maxHttpSessions 1000

503, X-RateLimit-Scope: sessions

Sessions per client key

maxHttpSessionsPerClient 10

the least-recently-used session of the same key idle for 60 s or more is closed to make room — behind one NAT or proxy that can be another user's; otherwise 503, X-RateLimit-Scope: client-sessions

Idle time

httpSessionIdleTtlMs 30 min

the session is closed (swept every 30 s); a request in flight keeps it open

initialize

60 s

a session that has not finished initializing is reclaimed; its request gets 503 sessions, retry

GET stream

30 min

the stream is closed; the TypeScript SDK client reopens it, other clients may need to

A client-sessions 503 usually means many users share one key — they reach the server through one proxy or NAT. Configure trustProxy (below) first. Raising maxHttpSessionsPerClient instead removes the per-client bound for everyone behind that key.

Memory: an idle session costs about 22 KB plus 1.75 KB per registered tool of heap, so a handler at its 1000-session cap holds about 22 / 40 / 111 MB with 0 / 10 / 50 tools. That is a floor: open GET streams add a socket each, and requests in flight add their own memory.

Sessions live in the process. Behind a load balancer with several instances, route by Mcp-Session-Id (session affinity); a session that reaches another instance gets 404. A restart ends every session.

Shutting down: the server factories' close() closes every session first (a GET whose client stopped reading is destroyed after 5 s). With createSecureHttpHandler, call handler.close() before your server's close(); server.shutdown() closes every attached handler's sessions too. server.close() and server.isConnected() describe the stdio connection only — they are not a readiness signal for HTTP.

Sessions end; hosts must recover

Every way a session ends reaches its client as 404: DELETE, the idle sweep, the per-client eviction above, a request that failed with 500, a reclaimed initialize, a restart. A host must then re-initialize — the protocol requires it, and stock SDK clients do not do it by themselves — and:

  • discard the old session id. A transport rebuilt with a stored sessionId resends it, skips initialize, and gets 404 again, indefinitely (the server does not throttle 404s);

  • not replay blindly a call that was in flight. It may already have run.

Hosts differ. Claude Code re-initializes on 404 by itself (leaving up to two orphaned sessions per recovery, which the idle sweep and the per-client eviction reclaim). The MCP Inspector does not; reconnect it by hand. Neither sends DELETE when it ends, so their sessions end by the idle time. A host that creates clients and never closes them holds their sessions, and their per-key slots, until its process exits.

Registrations freeze when a handler is attached

createSecureHttpHandler, createHttpServer and both server factories freeze the server's registrations when they return. Register every tool, resource and prompt first. After that, every registration method, registerCapabilities(), and every call or field write through a returned handle (update, disable, enable, remove) throws LateRegistrationError and changes nothing — on stdio too, for the same server object. Each session replays the frozen set; a change after the first session would reach some clients and not others.

  • Dynamic tools are not supported once a handler is attached. A tool that toggles another at runtime surfaces as a tool error to its client, and the operator gets a MCP_SECURE_SERVER_LATE_REGISTRATION warning (counted at 1, 10, 100, …).

  • A kill switch is a flag checked inside the tool's callback. Catching LateRegistrationError around disable() leaves the tool callable.

  • Rebuilding the server to change its tools resets its Layer 3 window and empties Layer 4's chains and quotas.

  • Escape hatches stay on stdio. A request handler set through server.server, or a tool registered on server.mcpServer, reaches the stdio connection only, never an HTTP session, so neither is a way around the freeze (the second also skips Layer 5's response wrapping). Reading either getter on a server with an HTTP handler warns once (MCP_SECURE_SERVER_ESCAPE_HATCH).

  • A registration that throws (a duplicate name, say) can leave the stdio-side server partly changed — the SDK's behaviour. HTTP sessions never see the partial change: only registrations that succeed are recorded.

  • Instances passed in options.server (a task store, for example) are shared by every session's server.

Inside a tool: the calling session

A tool handler's extra identifies the session that called it:

  • extra.sendRequest(...) is how a tool asks its client for sampling or elicitation. server.server.createMessage() goes to the stdio connection, which an HTTP-only server does not have.

  • server.sendLoggingMessage(params, extra.sessionId) reaches that session only, filtered by its logging/setLevel. Without an id it goes to the stdio connection. An id that names no live session — one that has ended, say — is delivered nowhere: falling back to the stdio client would hand one user's message to another, past that client's level filter. Undeliverable messages warn once (MCP_SECURE_SERVER_HTTP_LOGGING). It never broadcasts to every HTTP session, since sessions may belong to different users.

  • server.getClientCapabilities(extra.sessionId) and getClientVersion(extra.sessionId) answer for that session's client; without an id, for the stdio client.

  • Large sampling results need maxBodySize raised, which raises it for every POST.

  • A client's responses to the server's requests are checked for shape (a JSON-RPC result or error with an id) and metered at 600 per minute per session (X-RateLimit-Scope: responses); their content is not inspected.

Browser clients: Origin and Host

A request carrying an Origin not in allowedOrigins gets 403; a request without Origin (non-browser clients: Claude Code, SDK clients, curl) is allowed. The default list is empty, so every browser origin is refused until you list it.

  • Exact serialized origins: scheme://host[:port], lowercase, no path, no trailing slash, no default port. A malformed entry throws at construction. "null" cannot be listed: it is the origin of every sandboxed frame and file:// page, so listing it would admit them all.

  • Local development: list the dev server's origin exactly as the browser sends it — http://localhost:5173 and http://127.0.0.1:5173 are different origins.

  • Electron renderers, browser extensions and webviews send Origin and must be listed. A context with an opaque origin (a renderer on file://) sends Origin: null and cannot be listed: connect from Electron's main process, which sends no Origin, or serve the renderer from a listed origin.

  • allowedHosts adds an exact Host check: defence against DNS rebinding where a proxy has stripped Origin. It does not restrict non-browser clients, which can send any Host. [] throws.

CORS is yours to add, in a wrapper in front of the handler. Echo the request's Origin only when it is listed (never *, never null), pass the same list as allowedOrigins, allow GET, POST, DELETE, OPTIONS and the Mcp-Session-Id request header, and expose Mcp-Session-Id so a page can read it. The wrapper must never delete or rewrite Origin: the handler validates it, and a wrapper that strips it turns the check off for every page on the machine. The http-session-server and multi-endpoint-server examples carry a complete wrapper.

Warnings

The package reports conditions that are not errors through one sink per server:

Code

When

MCP_SECURE_SERVER_HTTP_EXPERIMENTAL

once per process, when a handler is created

MCP_SECURE_SERVER_HTTP_EXPOSED

once per handler, when its listener is bound beyond loopback

MCP_SECURE_SERVER_LATE_REGISTRATION

a registration or handle change after the freeze (counted at 1, 10, 100, …)

MCP_SECURE_SERVER_ESCAPE_HATCH

server.server, server.mcpServer or server.experimental was used on a server with an HTTP handler (a use before attaching is reported when it attaches)

MCP_SECURE_SERVER_HTTP_LOGGING

sendLoggingMessage was given an id that names no live session, or had no id and no stdio connection (each once)

MCP_SECURE_SERVER_TRUST_PROXY

a trustProxy misconfiguration a request revealed (see below)

By default they go to process.emitWarning, which NODE_NO_WARNINGS=1 hides. In production, route them to your logger, and silence a code only by naming it:

const server = new SecureMcpServer({ name: 'my-server', version: '1.0.0' }, {
  onWarning: (message, code) => logger.warn({ code }, message),
  silenceWarnings: ['MCP_SECURE_SERVER_HTTP_EXPERIMENTAL'] // each code named; there is no "all"
});

An unknown code in silenceWarnings, or an onWarning that is not a function, throws at construction.

What is per client, and what is not

Layer 3 is shared by every client of a server until 0.2.0. One client can exhaust the request budget for all, and Layer 3 counts every message: a short client is three (initialize, notifications/initialized, one call). Under the standard preset (a burst of 10 per 10 s), three clients arriving together fit and the fourth is refused. Hosts must back off, with jitter, and retry an initialize answered 429; nothing in 0.1.0 rewards the host that backs off over the one that retries first. A 429 on notifications/initialized leaves a session its client abandons; it holds a per-key slot until it has been idle 60 s (when the per-client eviction can take it) or until the idle time. For several clients over HTTP, raise Layer 3's limits (maxRequestsPerMinute, burstThreshold) or use the basic preset until 0.2.0. Layer 4's per-tool quotas are shared the same way. The opt-in Layer 5 per-client limit (contextual.rateLimiting) does not prevent either: it runs after Layers 3 and 4 have counted the request. Only a per-client limiter in front of the server (at your proxy) bounds one client today.

What is per client (keyed on the rate-limit key, below): the session cap and its eviction, the sessionlessRequests* GET/DELETE limits, and the error lockout. The lockout throttles repeated malformed requests from a key — their refusals become 429s. It never refuses a well-formed request, so it is not a per-client budget.

Telling what happened:

Status

X-RateLimit-Scope

Meaning

429

errors

the error lockout: this key sent too many malformed requests

429

sessionless

this key's GET/DELETE limit

429

not-found

this key's 404s on paths other than the endpoint (server factories)

429

responses

this session's client sent more than 600 responses a minute

503

sessions

the handler's session cap, a reclaimed initialize, or shutdown

503

client-sessions

this key's session cap — see trustProxy first

A 429 without that header came from the validation pipeline (Layer 3 or 4, or the Layer 5 per-client limit — the error's data.reason says which); trustProxy affects only the transport's.

Client IP and trustProxy

Every per-client limit on the HTTP path — the per-client session cap and its eviction, the error lockout, the 404 lockout, the sessionlessRequests* ceilings, and the opt-in Layer 5 rate limit — keys on one value resolved from the connection: the client's rate-limit key. It is derived from an address (see Keys), but it is not one. X-Forwarded-For and X-Real-IP are written by whoever sends the request, so they are only read through hops you tell the server to trust:

trustProxy

Key derived from

Use when

0 (default)

the TCP peer; forwarding headers ignored

the server is exposed directly

['10.0.0.12', '10.0.0.13'] (addresses or CIDRs of your proxies)

walking X-Forwarded-For right to left from the peer, the first address not in the list

preferred behind proxies whose addresses you know

N (hop count)

the Nth X-Forwarded-For entry from the right

proxy addresses are not stable and every request crosses exactly N appending proxies

// nginx on 10.0.0.12 in front of the app
server.createHttpServer({ endpoint: '/mcp', trustProxy: ['10.0.0.12'] });

Every address in the list is treated as a proxy. A request from a listed address is keyed on the X-Forwarded-For entries it sends, so the list must contain only your proxies. A request from an address not in the list is keyed on that address, however many entries it forges. The named ranges 'loopback', 'linklocal' and 'uniquelocal' (all of 10/8, 172.16/12, 192.168/16 and fc00::/7) are safe only when no client can reach the server from those ranges. On an intranet, VPN or container network, clients connect from private addresses, and a range like 'uniquelocal' lets each of them choose its own key. List your proxies' exact addresses, or the narrowest CIDR that contains only them.

The hop-count form trusts the topology blindly. It trusts every direct peer, so use it only for a server no client can reach except through exactly that many appending proxies. If the app port is reachable without going through the proxy, or N is higher than the real number of appending proxies, clients can choose their key again. A request carrying fewer entries than N is keyed on the TCP peer, never on the leftmost entry.

Preconditions either form relies on:

  • Your proxy must append to X-Forwarded-For, not pass the client's value through. nginx: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; (not $http_x_forwarded_for). A proxy that sets only X-Real-IP gives the server nothing it can trust; requests are keyed on the proxy. No server setting can recover a client address the proxy never recorded.

  • Count every appending hop. Cloudflare in front of an ALB is two hops, not one. An undercount keys clients on a CDN edge address, so users routed through one edge share a budget, and it is silent — no warning fires.

  • The listener needs a TCP peer. Behind a proxy connecting over a Unix socket, the server sees no peer address: every request is keyed 'unknown' (one shared budget), trustProxy has no effect, and nothing warns.

Keys. The key is the client address after normalization: ports and brackets stripped (203.0.113.1:5555 and [2001:db8::1]:443 become bare addresses), IPv4-mapped IPv6 (::ffff:203.0.113.1) treated as IPv4, and an entry that is not an IP replaced by the nearest trusted hop. IPv6 clients are keyed by their /64, the smallest block a subscriber is routinely assigned — keying each address would let one client rotate through 2^64 keys. The /64 is not one client in either direction: a holder of a /56 or /48 controls 256–65,536 keys, and clients behind one NAT or CGNAT address share one key. Tracked keys are capped at 10,000 per limiter window (oldest evicted). Because the value is a key (2001:db8:0:0::/64, or 'unknown' without a peer address), do not use it for allowlists, denylists or geolocation.

Each per-key 429 from the transport is logged with its key when security logging is on; the scope header says which limit (see Telling what happened above).

Misconfiguration emits a one-time warning per handler (code MCP_SECURE_SERVER_TRUST_PROXY, through the server's onWarning) in three cases: a private-range peer sends untrusted X-Forwarded-For (the message names everything those clients then share: one session cap, one eviction pool, one GET/DELETE limit and one lockout); a request has fewer entries than the hop count (raising N does not silence this one — only the correct value does); or a trusted proxy sends none. It cannot detect NAT, a proxy with a public address that you have not trusted, or an undercount, so no warning does not mean the configuration is right. The warnings come from requests to the MCP endpoint; requests to other paths (404s) are keyed the same way but do not warn. When building a resolver yourself (HTTP Utilities), createClientIpResolver({ trustProxy, onWarning }) takes its own sink: a function (message, kind) — kind is 'ignored', 'invalid' or 'short', each reported at most once per resolver — or false to silence that resolver. A handler reports each kind once too, so one misconfiguration does not hide another.

trustProxy: true and malformed values throw TypeError when the server is created.

Factories

Standalone function:

import { SecureMcpServer, createSecureHttpServer } from 'mcp-secure-server';

const server = new SecureMcpServer({ name: 'x', version: '1.0' }, {
  toolRegistry: [{ name: 'echo', sideEffects: 'none' }]
});
server.tool('echo', 'Echo', { text: z.string() }, async ({ text }) => ({ content: [{ type: 'text', text }] }));
const httpServer = createSecureHttpServer(server, { endpoint: '/api/mcp' }); // freezes registrations
httpServer.listen(8080, '127.0.0.1');

Multiple endpoints:

For services exposing multiple MCP servers on different paths, use createSecureHttpHandler to compose your own routing:

import { SecureMcpServer, createSecureHttpHandler } from 'mcp-secure-server';
import { createServer } from 'node:http';

// Create separate MCP servers with different tools/permissions
const adminServer = new SecureMcpServer({ name: 'admin', version: '1.0' }, {
  toolRegistry: [{ name: 'delete-user', sideEffects: 'write' }]
});
const publicServer = new SecureMcpServer({ name: 'public', version: '1.0' }, {
  toolRegistry: [{ name: 'get-status', sideEffects: 'none' }]
});

// Register tools on each server BEFORE creating its handler (creating it freezes registrations)
adminServer.tool('delete-user', ...);
publicServer.tool('get-status', ...);

// One session handler per server (validates requests, routes each to its session)
const adminHandler = createSecureHttpHandler(adminServer);
const publicHandler = createSecureHttpHandler(publicServer);

// Compose with custom routing
const httpServer = createServer(async (req, res) => {
  if (req.url?.startsWith('/api/admin')) return adminHandler(req, res);
  if (req.url?.startsWith('/api/public')) return publicHandler(req, res);
  res.writeHead(404).end(JSON.stringify({ error: 'Not found' }));
});

httpServer.listen(3000, '127.0.0.1'); // no authentication: '/api/admin' is open to anything that reaches the port

// Shutting down: close every session, then the listener
await Promise.all([adminHandler.close(), publicHandler.close()]);
httpServer.close();

The multi-endpoint-server example is this, with a CORS wrapper and tests.

Function

Purpose

createSecureHttpServer

Single endpoint, includes routing

createSecureHttpsServer

Same, with TLS (key/cert) — recommended for production

createSecureHttpHandler

Request handler only, you provide routing; returns a handler with close() and sessionCount

CORS: a wrapper in front of the handler — see Browser clients above; it must never delete or rewrite Origin.

HTTPS: Use the built-in factory (or deploy behind a TLS-terminating reverse proxy):

import { readFileSync } from 'node:fs';
import { createSecureHttpsServer } from 'mcp-secure-server';

const httpsServer = createSecureHttpsServer(server, {
  key: readFileSync('server.key'),    // TLS private key (PEM string or Buffer)
  cert: readFileSync('server.cert'),  // TLS certificate (PEM string or Buffer)
  ca: undefined,                      // optional CA chain
  endpoint: '/mcp'
});
httpsServer.listen(3443, '127.0.0.1'); // TLS is not authentication

Available Exports

import {
  SecureMcpServer,            // Main secure server class
  LateRegistrationError,      // Thrown by a registration after an HTTP handler is attached
  SecureTransport,            // Transport wrapper
  createSecureHttpServer,     // HTTP server factory (single endpoint)
  createSecureHttpHandler,    // HTTP handler factory (multi-endpoint)
  createSecureHttpsServer,    // HTTPS server factory with TLS
  ErrorRateLimiter,           // Rate limiter for error responses
  getClientIp,                // Resolve a request's rate-limit key (one-off)
  createClientIpResolver,     // Build a validated rate-limit key resolver once
  ContextualValidationLayer,  // Layer 5 class
  ContextualConfigBuilder,    // Builder for Layer 5 config
  createContextualLayer,      // Factory for Layer 5
  // Tool policy configuration
  initializeToolPolicies,     // Load config from object
  resetToolPolicies,          // Reset to defaults
  registerToolPolicy,         // Register policy at runtime
  getToolPolicy,              // Get policy for a tool
  isRelaxedField,             // Check if field has relaxed validation
  getToolsByLevel,            // List tools by security level
  isValidSecurityLevel,       // Validate security level string
  defaultToolPolicies,        // Default policy definitions
  getToolPoliciesConfig,      // Get current policies config
  loadToolPoliciesConfig,     // Load from file
  matchesPattern,             // Check if tool matches pattern
  resolvePolicy,              // Resolve policy for tool
  ToolPolicyError,            // Config error class
  // Security presets
  SECURITY_PRESETS,           // All preset definitions
  resolvePreset,              // Get preset by name
  getDefaultPreset,           // Get default preset name
  isValidPreset,              // Validate preset name
  // Type guards
  isSeverity,                 // Type guard for Severity
  isViolationType,            // Type guard for ViolationType
  isError,                    // Type guard for Error objects
  getErrorMessage             // Safe error message extraction
} from 'mcp-secure-server';

Export

Description

SecureMcpServer

Drop-in replacement for McpServer with 5-layer security

LateRegistrationError

Thrown by a registration or handle change after an HTTP handler is attached; carries method and registrationName

SecureTransport

Transport wrapper for message-level validation

createSecureHttpServer

HTTP server factory with security validation

createSecureHttpHandler

HTTP session handler for composing multi-endpoint servers; returns a SessionHttpHandler (close(), sessionCount)

createSecureHttpsServer

HTTPS server factory with TLS certificates

ErrorRateLimiter

Rate limiter for clients generating excessive errors

getClientIp

Resolve a request's rate-limit key (socket peer; X-Forwarded-For only through trustProxy); validates on every call

createClientIpResolver

Build a key resolver once, validating trustProxy at construction; emits misconfiguration warnings

ContextualValidationLayer

Layer 5 class for advanced customization

ContextualConfigBuilder

Builder for Layer 5 configuration

createContextualLayer

Factory function for Layer 5 with defaults

initializeToolPolicies

Load tool policies from config object

resetToolPolicies

Reset to default (empty) policies

registerToolPolicy

Register single tool policy at runtime

getToolPolicy

Get resolved policy for a tool name

isRelaxedField

Check if a field has relaxed validation for a tool

getToolsByLevel

List tools registered at a specific security level

isValidSecurityLevel

Validate if a string is a valid security level

defaultToolPolicies

Default policy definitions for tools

getToolPoliciesConfig

Get current tool policies configuration

loadToolPoliciesConfig

Load policies from JSON file

matchesPattern

Check if tool name matches a pattern (glob-style)

resolvePolicy

Resolve merged policy for a tool from config

ToolPolicyError

Error class for config validation failures

SECURITY_PRESETS

Object containing all preset definitions (basic, standard, paranoid)

resolvePreset

Get preset configuration by name

getDefaultPreset

Get the default preset name ('standard')

isValidPreset

Validate if a string is a valid preset name

isSeverity

Type guard to check if value is a valid Severity

isViolationType

Type guard to check if value is a valid ViolationType

isError

Type guard to check if value is an Error object

getErrorMessage

Safely extract error message from unknown value

Subpath Imports

Everything is available from the root import, but two subpaths exist for consumers who want a narrower dependency surface (e.g. bundlers tree-shaking a transport-only usage):

import { SecureMcpServer } from 'mcp-secure-server/server';       // server class only
import { SecureTransport } from 'mcp-secure-server/transport';    // transport layer only

Both carry their own types conditions, so they resolve under moduleResolution: node16/nodenext.

HTTP Utilities

ErrorRateLimiter and createClientIpResolver are the building blocks the HTTP factories use internally — reach for them only when composing a custom handler around createSecureHttpHandler. Build the resolver once, at startup, with the same trustProxy you give the handler, so a bad value fails before the first request and both limiters agree on who the client is:

import { createServer } from 'node:http';
import { SecureMcpServer, createSecureHttpHandler, createClientIpResolver, ErrorRateLimiter } from 'mcp-secure-server';

const server = new SecureMcpServer({ name: 'my-server', version: '1.0.0' });
// ... register every tool here: creating the handler freezes registrations ...
const trustProxy = ['10.0.0.12'];                              // your proxy's address; one value, shared
const handler = createSecureHttpHandler(server, { trustProxy }); // (req, res) => Promise<void>, plus close()
const clientIp = createClientIpResolver({ trustProxy });       // throws here if trustProxy is invalid
const limiter = new ErrorRateLimiter();                        // throttles clients spamming invalid requests

// Transport-level, client-specific. Not 400: clients send a well-formed request before they
// initialize (Claude Code probes with `server/discover`). Not 404: an expired session is not abuse.
const CLIENT_ERRORS = new Set([405, 408, 415]);

createServer(async (req, res) => {
  const key = clientIp(req);                                   // a rate-limit key, not an address; resolve before any await
  if (limiter.shouldRateLimit(key)) { res.writeHead(429).end(); return; }
  await handler(req, res);
  // Count only rejections caused by this client. Pipeline 4xx/429s can come from the
  // server-wide Layer 3/4 budgets; counting them would lock out every client at once.
  if (CLIENT_ERRORS.has(res.statusCode)) limiter.recordError(key);
});

ErrorRateLimiter options (all optional):

Option

Default

Meaning

errorsPerMinute

20

errors a key may record per minute before shouldRateLimit returns true

errorsPerHour

100

the same, per hour

cleanupIntervalMs

60000

how often expired counters are swept

maxTrackedClients

10000

keys tracked per window; the oldest is evicted beyond this, bounding memory

getClientIp(req, { trustProxy }) is the one-off form: it re-validates on every call (so a bad value throws per request) and never warns.

Layer 5 Customization

Layer 5 is enabled by default. You can add custom validators at runtime for application-specific security rules.

Adding Custom Validators

import { SecureMcpServer } from 'mcp-secure-server';

const server = new SecureMcpServer(
  { name: 'my-server', version: '1.0.0' },
  {
    contextual: {
      domainRestrictions: {
        enabled: true,
        blockedDomains: ['evil.com']
      }
    }
  }
);

// Access Layer 5
const layer5 = server.validationPipeline.layers[4];

// Add custom validator with priority (lower = runs first)
layer5.addValidator('sensitive-data-check', (message, context) => {
  if (message.params?.arguments?.creditCard) {
    return {
      passed: false,
      reason: 'Credit card data not allowed in requests',
      severity: 'HIGH',
      violationType: 'SENSITIVE_DATA'
    };
  }
  return { passed: true };
}, { priority: 50, failOnError: true });

Exceptions fail closed. Since 0.0.21, failOnError defaults to true for validators, global rules and response validators: a validator that throws blocks the request (or the response) with VALIDATOR_ERROR, matching Layers 1–4. Pass failOnError: false for an advisory validator whose failure should not block. Before 0.0.21 the default was false, so a throwing authorization or rate-limit validator silently admitted the request.

Adding Global Rules

Global rules run before validators and can short-circuit validation.

layer5.addGlobalRule((message) => {
  // Block specific operations
  if (message.method === 'admin/delete-all') {
    return {
      passed: false,
      reason: 'Operation not permitted',
      severity: 'CRITICAL',
      violationType: 'POLICY_VIOLATION'
    };
  }
  return null;  // null = pass, continue to validators
});

Adding Response Validators

Validate responses before they're sent to clients.

layer5.addResponseValidator('pii-filter', (response) => {
  const content = JSON.stringify(response);

  // Check for SSN pattern
  if (/\d{3}-\d{2}-\d{4}/.test(content)) {
    return {
      passed: false,
      reason: 'PII detected in response',
      severity: 'HIGH',
      violationType: 'DATA_LEAK'
    };
  }
  return { passed: true };
});

A response validator is called as (response, request, context): request is { tool, arguments }, and context.sessionId is the calling HTTP session's id (a bearer credential: hash it before logging), or undefined over stdio. Until 0.1.0 context was always {}, so a validator keyed on context.sessionId saw one shared key on every transport.

Using Context Store

Cross-request state management with TTL support.

context.sessionId is not an identity. Over HTTP it is the session id the server issued (until 0.1.0 it was whatever Mcp-Session-Id the client sent, or 'stateless'); over stdio it is undefined. It names a session, not a user: there is no authentication, so anyone can start one, and the id is a bearer credential — whoever holds it is that session. Log a hash of it, never the id. Do not key authentication or authorization state on it. Key such state on an identity your own authentication established, and treat context.clientId (the HTTP rate-limit key) as a throttling key, not a user.

// Set context with 5-minute TTL, keyed on an identity YOUR auth layer verified
layer5.setContext(`user:${verifiedUserId}`, {
  authenticated: true,
  roles: ['admin']
}, 300000);

// Get context
const user = layer5.getContext(`user:${verifiedUserId}`);

// Use in validators — resolveVerifiedUser is yours (e.g. from a validated token), not a header
layer5.addValidator('auth-check', (message, context) => {
  const session = layer5.getContext(`user:${resolveVerifiedUser(context)}`);
  if (!session?.authenticated) {
    return {
      passed: false,
      reason: 'Authentication required',
      severity: 'HIGH',
      violationType: 'AUTH_REQUIRED'
    };
  }
  return { passed: true };
});

Disabling Layer 5

const server = new SecureMcpServer(
  { name: 'my-server', version: '1.0.0' },
  { contextual: { enabled: false } }
);

Security Features

See SECURITY.md for full security documentation including:

  • Attack detection coverage (injection, XSS, SSRF, deserialization, etc.)

  • Security best practices applied

  • SSRF protection details

  • Error sanitization

  • Reporting vulnerabilities

Attack Coverage

The framework detects and blocks 200+ attack patterns across 19 categories including injection attacks, path traversal, SSRF, deserialization, and more.

See SECURITY.md for the complete threat model with:

  • Attack vectors and examples

  • Severity levels and detection layers

  • Mitigation strategies

  • Violation type reference

Error Handling

Error Message Behavior

Error messages are deterministic and type-specific — each violation type maps to a consistent, descriptive message. This makes errors diagnosable from logs, MCP clients, and LLM tool-use contexts.

enableDetailedErrors

error.message

data.reason

false (default)

Type-specific (e.g. "Request validation failed")

Redacted internal reason

true

Type-specific + redacted reason appended

Redacted internal reason

When enableDetailedErrors is true, the error.message field includes the redacted reason so the full diagnostic is visible without digging into the data envelope:

{
  "error": {
    "code": -32602,
    "message": "Request validation failed: Content validation — path traversal pattern detected in parameter 'file_path'",
    "data": {
      "reason": "Content validation — path traversal pattern detected in parameter 'file_path'",
      "layer": "VALIDATION_ERROR"
    }
  }
}

When enableDetailedErrors is false, the top-level message is the category only — the detailed reason is still available in data.reason:

{
  "error": {
    "code": -32602,
    "message": "Request validation failed",
    "data": {
      "reason": "Content validation — path traversal pattern detected in parameter 'file_path'",
      "layer": "VALIDATION_ERROR"
    }
  }
}

Validation Error Structure

When the security pipeline blocks a request, the framework returns a JSON-RPC error with diagnostic context in the data field:

Field

Description

timestamp

When the error occurred (ISO 8601)

token

Unique error token for log correlation

reason

Redacted validation reason describing what failed and why

layer

Which validation layer or violation type triggered the block

retryAfterMs

Present only for RATE_LIMIT_EXCEEDED — milliseconds to wait

The reason field is sanitized through the same credential/PII redaction pipeline used for logging, so it is safe to surface to clients while still providing actionable diagnostics.

Error Codes

Code

Violation Type

Meaning

-32602

VALIDATION_ERROR, POLICY_VIOLATION, CONTEXT_VIOLATION

Invalid input or policy block

-32000

RATE_LIMIT_EXCEEDED

Too many requests — check retryAfterMs

-32603

INTERNAL_ERROR

Internal validation error

Severity Levels

Severity

Description

Action

CRITICAL

Active exploit attempt (command injection, deserialization)

Block + Alert

HIGH

Serious attack (SQL injection, path traversal)

Block

MEDIUM

Suspicious activity (rate limit, size exceeded)

Block

LOW

Minor policy violation

Block or Warn

Outgoing Response Sanitization

On both the stdio connection and every HTTP session (HTTP since 0.1.0), the framework sanitizes outgoing JSON-RPC error responses that contain Zod validation patterns, with one important exception: -32602 (Invalid params) errors are preserved. These errors describe the caller's input mistakes — field paths, expected types, and size limits — which are the input contract, not internal implementation details. Preserving them lets callers diagnose and fix their requests.

For non--32602 errors that contain Zod patterns (e.g., internal -32603 errors), the framework replaces the response with a safe generic message:

{
  "error": {
    "code": -32602,
    "message": "Invalid input parameters",
    "data": {
      "reason": "Input failed schema validation (Zod). Check parameter types and required fields.",
      "layer": "OUTGOING_SANITIZER"
    }
  }
}

This prevents internal Zod schema structures from leaking to clients while preserving actionable validation feedback on input errors.

Nested Tool Input Errors

Version 0.0.22-security expands serialized Zod 3 unionErrors and Zod 4 errors when one union branch has a uniquely deeper field path. For example, an invalid summary can name analysis_summary.exploration_maps.0.metadata.framework rather than only analysis_summary. Equal-depth alternatives and malformed diagnostics retain their original union message. Expansion stops at 20 nested union levels. The rewrite preserves JSON-RPC IDs/error codes and isError tool-result envelopes.

This requires branch details to reach the sanitizer. MCP SDK 1.30 formats only its top-level issues, so nested details can be lost before this package sees them. For affected schemas, use the schema library's public error customization API to include actionable nested paths in the union message, or an upstream formatter that preserves branches. The sanitizer cannot reconstruct discarded information. This release does not change tool schemas or how a host displays them.

Type Guards for Error Handling

import { isError, getErrorMessage, isSeverity } from 'mcp-secure-server';

try {
  await server.connect(transport);
} catch (error) {
  if (isError(error)) {
    console.error('Error:', getErrorMessage(error));
  }
}

// Validate severity values
const severity = 'HIGH';
if (isSeverity(severity)) {
  // TypeScript knows severity is Severity type
}

Claude Desktop Integration

Add to your Claude Desktop configuration (claude_desktop_config.json):

{
  "mcpServers": {
    "secure-server": {
      "command": "node",
      "args": ["path/to/your/server.js"],
      "cwd": "/path/to/project"
    }
  }
}

Test Server

Run the included test server to verify the framework:

npm start

The test server includes 7 protected tools:

  • debug-calculator - Basic math operations

  • debug-file-reader - Safe file reading

  • debug-echo - Text echo service

  • debug-database - Database query simulation

  • debug-http - HTTP request simulation

  • debug-parser - JSON/XML parsing

  • debug-image - Image processing simulation

Add to Claude Desktop:

{
  "mcpServers": {
    "secure-test": {
      "command": "npx",
      "args": ["tsx", "cookbook/minimal-server/minimal-test-server.ts"],
      "cwd": "/path/to/mcp-secure-server"
    }
  }
}

Development

Running Tests

# Run all tests
npm test

# Run specific test suites
npm run test:unit
npm run test:integration
npm run test:performance

# Typecheck + full suite against the newest published MCP SDK 1.x, in a scratch copy
# (npm test runs the locked version; this repo has no CI to run the second column)
npm run test:sdk-latest                 # or: npm run test:sdk-latest -- 1.31.0

# Cookbook examples (needs `cd cookbook && npm install` once; part of prepublishOnly)
npm run test:cookbook                   # typecheck + tests for every example, offline
COOKBOOK_LIVE=1 npm run test:cookbook   # also run tests that call live public APIs

# Watch mode for development
npm run test:watch

# Generate coverage report
npm run test:coverage

Test Coverage:

  • Overall: 93.76% lines, 87.78% branches (npm run test:coverage)

  • Unit, integration and type-level tests, including test/integration/dist-smoke.test.ts, which imports the compiled dist/ in a separate node process

  • Mutation tests for severity levels

  • Boundary value tests for limits

  • Real attack vector validation

Running a Single Test

npx vitest run test/unit/utils/canonical.test.js

Linting

npm run lint

Project Structure

src/
├── index.ts                              # Main entry point & public exports
├── types/                                # TypeScript type definitions
│   ├── index.ts                          # Type exports & guards
│   ├── layers.ts                         # Layer type definitions
│   ├── messages.ts                       # MCP message types
│   ├── policies.ts                       # Policy type definitions
│   ├── server.ts                         # Server configuration types
│   └── validation.ts                     # Validation result types
└── security/
    ├── index.ts                          # Security module exports
    ├── mcp-secure-server.ts              # SecureMcpServer class
    ├── constants.ts                      # Configuration constants
    ├── transport/
    │   ├── index.ts                      # Transport exports
    │   ├── secure-transport.ts           # SecureTransport (the connect() connection)
    │   └── http-server.ts                # HTTP transport server
    ├── layers/
    │   ├── validation-layer-base.ts      # Base class for all layers
    │   ├── layer1-structure.ts           # JSON-RPC validation
    │   ├── layer2-content.ts             # Content/injection detection
    │   ├── layer2-validators/            # Modular content validators
    │   │   ├── index.ts                  # Validator exports
    │   │   ├── pattern-detection.ts      # Attack pattern matching
    │   │   ├── base64-css.ts             # Base64/CSS attack detection
    │   │   └── data-semantics.ts         # Data format validation
    │   ├── layer3-behavior.ts            # Rate limiting & burst detection
    │   ├── layer4-semantics.ts           # Tool contracts & policies
    │   ├── layer5-contextual.ts          # Custom validators
    │   ├── contextual-config-builder.ts  # Layer 5 fluent configuration
    │   └── layer-utils/
    │       ├── content/
    │       │   ├── canonicalize.ts       # Text normalization
    │       │   ├── unicode.ts            # Unicode attack normalization
    │       │   ├── dangerous-patterns.ts # Pattern configuration
    │       │   ├── helper-utils.ts       # Content helper functions
    │       │   ├── patterns/             # Attack pattern definitions
    │       │   │   ├── index.ts          # Pattern exports & utilities
    │       │   │   ├── injection.ts      # SQL/XSS/NoSQL patterns
    │       │   │   ├── path-traversal.ts # Path traversal patterns
    │       │   │   ├── network.ts        # SSRF/network patterns
    │       │   │   └── overflow-validation.ts # Buffer/encoding patterns
    │       │   └── utils/
    │       │       ├── index.ts          # Utility exports
    │       │       ├── text-decoding.ts  # Encoding detection
    │       │       ├── hash-utils.ts     # Cache key generation
    │       │       └── structural-analysis.ts # Deep structure analysis
    │       └── semantics/
    │           ├── semantic-policies.ts  # Tool/resource policies
    │           └── semantic-quotas.ts    # Quota management
    └── utils/
        ├── validation-pipeline.ts        # Multi-layer orchestration
        ├── security-logger.ts            # Security event logging
        ├── error-sanitizer.ts            # Safe error responses
        ├── request-normalizer.ts         # Request normalization
        ├── response-validator.ts         # Response validation
        └── tool-registry.ts              # Tool management

cookbook/                                 # Example MCP servers
├── http-server/                          # HTTP transport example
├── multi-endpoint-server/                # Multi-endpoint routing
├── image-gen-server/                     # Image generation APIs
├── kenpom-server/                        # Sports analytics API
├── nba-server/                           # NBA statistics API
├── api-wrapper-server/                   # Safe external API wrapper
├── database-server/                      # SQL injection prevention
├── filesystem-server/                    # Path traversal prevention
├── cli-wrapper-server/                   # Command injection prevention
├── monitoring-server/                    # Security metrics & alerts
├── transaction-server/                   # State machine workflows
└── advanced-validation-server/           # Advanced security demos

Troubleshooting

Module Not Found

Error: Cannot find module '@modelcontextprotocol/sdk'

Solution: @modelcontextprotocol/sdk is a regular dependency of this package, so this means a broken install. Reinstall:

npm install mcp-secure-server

Rate Limit Exceeded

Error: Request blocked: Rate limit exceeded

Solution: Increase rate limits in configuration:

{
  maxRequestsPerMinute: 60,
  maxRequestsPerHour: 1000
}

Every Client Gets 429 Behind a Proxy

HTTP 429 Too many requests   (X-RateLimit-Scope: sessionless | errors | not-found)
HTTP 503 Too many sessions for this client   (X-RateLimit-Scope: client-sessions)
Warning: X-Forwarded-For arrived from 10.0.0.5, a private address that trustProxy does not trust ...

Solution: since 0.0.26 per-client limits ignore X-Forwarded-For unless trustProxy names your proxies, so everyone behind the proxy shares one budget. Set trustProxy — see Client IP and trustProxy. Raising the limits instead keeps the shared budget and only delays the next lockout; raising maxHttpSessionsPerClient removes the per-client session bound for everyone behind the proxy.

False Positive Detection

Error: Request blocked: Path traversal detected

Solution: If legitimate path contains ../, configure resource policy:

{
  resourcePolicy: {
    rootDirs: ['./allowed-paths'],
    // Paths are validated relative to rootDirs
  }
}

Logging Not Working

getVerboseSecurityReport() returns empty

Solution: Enable logging in configuration:

{
  enableLogging: true,
  verboseLogging: true
}

Log Files Missing or Empty (logging enabled)

enableLogging: true, but no log files appear / getVerboseSecurityReport is empty

Cause: The logger silently degrades to no-op when the log directory cannot be created or written — e.g. MCP hosts that launch the server with cwd="/", where the default <cwd>/logs resolves to the unwritable /logs.

Diagnose: Check getSecurityStats().logger.fileLoggingAvailable. If false, logging is degraded; a one-time warning was also written to stderr at startup. writeErrors/lastWriteError flag failures that began after a healthy start.

Solution: Point logDir at a writable path (or set the LOG_DIR env var):

{
  enableLogging: true,
  logDir: '/var/log/my-mcp-server'  // or set LOG_DIR=/var/log/my-mcp-server
}

If the directory still cannot be created, security events are not recorded but the server continues operating.

Layer 5 Validators Not Running

Solution: Ensure Layer 5 is enabled:

{
  contextual: {
    enabled: true  // Must be true (default)
  }
}

TypeScript Type Errors

tsc very slow or out of memory around server.tool(...) calls: two copies of zod are installed (see TypeScript Support). Run npm ls zod and align on one version.

No overload matches this call on server.tool(...): since 0.0.24-security the handler's return type is checked against the SDK's CallToolResult. A result declared as an interface fails (interfaces have no implicit index signature) — declare it with type instead, or annotate the handler's return as CallToolResult.

Otherwise: Ensure you're using TypeScript 5.0+ with strict mode:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}

SQL Injection Detected (False Positive)

Error: Request blocked: SQL injection detected

Cause: Content contains SQL-like keywords (SELECT, DROP, UNION) in legitimate text.

Solutions:

  1. Use tool policies to relax validation for content storage tools:

{
  "version": "2.0",
  "tools": {
    "save_document": {
      "level": "STORAGE",
      "relaxedFields": ["content", "description"]
    }
  }
}
  1. Set the tool's security level programmatically:

import { registerToolPolicy } from 'mcp-secure-server';

registerToolPolicy('save_document', {
  level: 'STORAGE',
  relaxedFields: ['content']
});

Command Injection Detected (False Positive)

Error: Request blocked: Command injection detected

Cause: Content contains shell characters (|, ;, &&, backticks) or common command names (top, env, curl, grep) in legitimate text.

Note: As of v0.0.14, STORAGE-level tools no longer check command.systemInfo, command.basicInjection, command.networkOperations, or command.fileOperations patterns — only command.shellAccess and command.executionWrappers (which match actual shell invocations like bash -i, /bin/sh, system()). If you're seeing this on a STORAGE-level tool, upgrade to v0.0.14+.

Solution: Use STORAGE level for content tools:

registerToolPolicy('save_code_snippet', {
  level: 'STORAGE',
  relaxedFields: ['code']
});

XSS Attempt Detected (False Positive)

Error: Request blocked: XSS attempt detected

Cause: Content contains HTML/script tags in legitimate documentation.

Solution: Use DISPLAY level for read-only display tools:

registerToolPolicy('render_markdown', {
  level: 'DISPLAY'
});

Semantic Validation: Missing Required Parameter

Error: Request blocked: Missing required parameter

Cause: Tool is registered in toolRegistry with argsShape that doesn't match the request.

Solution: Verify tool schema matches expected parameters:

{
  toolRegistry: [{
    name: 'my-tool',
    argsShape: {
      query: { type: 'string', required: true },
      limit: { type: 'number' }  // optional
    }
  }]
}

Semantic Validation: Tool Not Registered

Error: Request blocked: Tool not registered

Cause: Tool called via MCP but not in server's toolRegistry.

Solutions:

  1. Register all tools you expose:

{
  toolRegistry: [
    { name: 'tool-a', sideEffects: 'none' },
    { name: 'tool-b', sideEffects: 'read' }
  ]
}
  1. There is no "allow all" setting. Unregistered tools are always denied at Layer 4 (TOOL_NOT_ALLOWED, fail closed). Omitting toolRegistry does not lift that — it installs the built-in registry, which contains only debug-calculator, debug-file-reader and debug-echo. (Until 0.0.21 this section claimed omission allowed all tools; it never did.)

Message Size Exceeded

Error: Request blocked: Message size exceeds limit

Solution: Increase message size limit:

{
  maxMessageSize: 100000  // 100KB (default: 50KB)
}

String Parameter Too Long

Error: Request could not be processed: String parameter too long: 9213 chars (max: 5000)

Cause: A single string argument exceeds the per-string cap (default: 5,000 chars). Common for tools that legitimately accept long text payloads — report markdown, document bodies, large descriptions.

Solution: Raise the per-string limit, and keep maxMessageSize above it — the message envelope is larger than the string it carries, so the size check fires first if you raise only the string cap:

{
  maxStringLength: 128 * 1024,  // 128K chars (default: 5,000)
  maxMessageSize: 500 * 1024    // envelope headroom above maxStringLength
}

Requires mcp-secure-server >= 0.0.17-security — earlier versions hardcode the default.

Burst Activity Detected

Error: Request blocked: Burst activity detected

Cause: Too many requests in the burst detection window.

Solutions:

  1. Increase burst threshold:

{
  burstThreshold: 20  // Max requests in window (default: 10)
}
  1. Extend the burst window:

{
  burstWindowMs: 15000  // 15 seconds (default: 10000)
}
  1. Use the basic preset for development:

{
  securityLevel: 'basic'  // 30 burst threshold, 10s window
}

Automated Timing Pattern Detected

Error: Request blocked: Automated timing pattern detected

Cause: Requests are arriving at suspiciously consistent intervals, suggesting automation.

Solutions:

  1. Disable automation detection (for legitimate automation):

{
  automationDetection: { enabled: false }
}
  1. Adjust detection thresholds:

{
  automationDetection: {
    enabled: true,
    maxVariance: 100,   // Allow more timing variance (default: 50ms)
    sampleSize: 10      // Require more samples (default: 5)
  }
}
  1. Use the basic preset (automation detection disabled):

{
  securityLevel: 'basic'
}

Suspiciously Large Message

Error: Request blocked: Suspiciously large message

Cause: Message exceeds the suspicious size threshold.

Solution: Increase the threshold:

{
  suspiciousMessageSize: 50000  // 50KB (default: 20KB)
}

License

MIT License - see LICENSE file for details.


Changelog

See CHANGELOG.md for version history and release notes.


Disclaimer: This framework provides defense-in-depth security but cannot guarantee protection against all attacks. Always follow security best practices and keep dependencies updated.

Available Tools

7 tools
debug-calculatorB

Simple calculator for testing

NameRequiredDescriptionDefault
expressionYesMath expression (supports +, -, *, /, parentheses)

TDQS

B3.1/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full burden for behavioral disclosure. It only says 'for testing,' implying a stateless, read-only operation, but does not confirm side effects, error behavior, or return format. The minimal description leaves significant ambiguity about how the tool behaves.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, short sentence with no unnecessary information. It is front-loaded and efficiently communicates the tool's role.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (one parameter, no output schema), the description is adequate but incomplete. It does not mention that the tool returns a result (e.g., numeric value) or handle errors. For a testing calculator, some indication of output would improve completeness.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% (single parameter 'expression' with a description of supported operators). The description adds no extra meaning beyond the schema, so it meets the baseline but does not enhance clarity or provide usage hints for the parameter.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly identifies the tool as a 'Simple calculator for testing,' which conveys its purpose and resource. It distinguishes itself from sibling debug tools (e.g., debug-database, debug-echo) by specifying it performs calculations. However, it lacks a specific verb like 'evaluate' and could be more precise about the resource (arithmetic expressions).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is provided on when to use this tool versus alternatives like debug-echo or debug-parser. There is no mention of when not to use it, prerequisites, or the testing context that might differentiate it. The description is purely declarative.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

debug-databaseC

Mock database operations using safe predefined queries

NameRequiredDescriptionDefault
operationYesPredefined safe operation
paramsNoOperation parameters

TDQS

C2.9/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided. Description mentions 'safe' and 'mock' but does not disclose behavior like read-only status, side effects, or permissions. Carries full burden but lacks detail.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Single sentence that is front-loaded and to the point. No unnecessary words, though could be slightly more structured.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema. Does not explain return values, error handling, or provide examples. For a tool with nested parameters and no annotations, the description is too minimal to be fully complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Input schema has 100% description coverage for the operation parameter with enums. The description reinforces 'safe predefined queries' but adds little beyond the schema. The params object is minimally described.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it performs mock database operations with safe predefined queries, distinguishing it from other debug tools. However, it could be more specific about the scope of operations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance on when to use this tool versus siblings. Does not specify conditions, prerequisites, or when not to use it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

debug-echoB

Echo tool for testing

NameRequiredDescriptionDefault
textYesText to echo back

TDQS

B3.3/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description minimally covers behavior by stating it is an 'echo tool for testing', implying no side effects. However, it does not explicitly confirm read-only behavior or any processing, leaving some ambiguity.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence with no wasted words. It gets straight to the point.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple tool with one parameter and no output schema or annotations, the description is adequate but lacks explicit details about output format or any transformations. It is minimally complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% with the 'text' parameter having a description ('Text to echo back'). The description adds no additional parameter meaning beyond the schema, so baseline score of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly identifies the tool as an echo tool for testing, which implies it returns input back. This is specific enough to distinguish from sibling debug tools like calculator or database, though it could be more explicit (e.g., 'returns the input text unchanged').

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is provided on when to use this tool versus alternatives, nor any prerequisites or limitations. The description lacks context for appropriate usage scenarios.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

debug-file-readerC

File reader for testing

NameRequiredDescriptionDefault
pathYesFile path

TDQS

C2.9/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full burden. It only states 'File reader for testing' without disclosing side effects, permissions, or behavior like file type support or error handling.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single concise sentence with no redundancy. It is appropriately front-loaded and efficient for a simple tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no output schema and no details on return values (e.g., file contents, errors), the description lacks completeness. For a testing tool, more context about expected output is needed.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% with the 'path' parameter described as 'File path'. The description adds no additional meaning beyond the schema, so baseline score of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description 'File reader for testing' clearly states the tool's action (reading files) and context (testing), distinguishing it from sibling debug tools that perform other operations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance on when to use this tool vs alternatives like debug-database or debug-echo. The description provides no usage context or exclusion criteria.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

debug-httpB

Mock HTTP request with URL validation (whitelist pattern)

NameRequiredDescriptionDefault
urlYesURL to request
methodNoHTTP method

TDQS

B3.2/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations, so description must carry burden. It reveals URL validation (whitelist pattern) and 'mock' implies non-destructive testing, but lacks details on failure behavior, side effects, or access limitations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single sentence front-loads the purpose and key constraint. Efficient with no wasted words, though could benefit from slight expansion on usage.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple tool with no output schema, the description covers purpose and a constraint but omits expected output format, error handling, or examples. Adequate but could be more complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3. The description adds minimal extra meaning beyond the schema; it does not explain how 'url' or 'method' interact with validation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool mocks an HTTP request with URL validation. It distinguishes from sibling tools (calculator, database, echo, etc.) by specifying HTTP mocking, making its purpose clear.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance on when or when not to use this tool versus alternatives. No context about scenarios where tool is appropriate or limitations like requiring a whitelist pattern.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

debug-imageC

Mock image processing with directory restrictions

NameRequiredDescriptionDefault
operationYesImage operation
filenameYesImage filename (no paths)
paramsNoOperation parameters

TDQS

C2.5/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It mentions 'mock' and 'directory restrictions' but does not detail behavior (e.g., return values, side effects, or the nature of the mock).

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is very concise (six words) with no waste, but it may be too terse for a tool with three parameters and no annotations. It is adequately sized for a simple tool but lacks important context.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description does not complete the picture: no output schema, no explanation of return values, and no details on the mock behavior or directory restrictions. Given the three parameters and lack of annotations, more context is needed.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the schema already documents all parameters. The description adds no additional meaning beyond implying directory restrictions. Baseline is 3, and no extra value is provided.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose3/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states 'Mock image processing with directory restrictions,' which gives a general idea but lacks specificity about the operations (e.g., resize, thumbnail, convert). It distinguishes from siblings (calculator, database, etc.) but does not use a clear verb+resource construction.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool, when not to, or how it compares to alternatives. There is no mention of prerequisites or exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

debug-parserC

JSON/XML parser with size limits

NameRequiredDescriptionDefault
dataYesJSON or XML string to parse (max 5KB)
formatYesData format

TDQS

C2.9/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description should disclose behavioral traits. It mentions size limits but does not state if parsing is read-only, whether it validates or transforms, or what happens on errors. This is insufficient for a tool that operates on input data.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single concise sentence with no fluff. It is appropriately front-loaded but misses some important details like output format or error behavior.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool is simple with two parameters, but without an output schema, the description should clarify what the parser returns. It does not explain the result format (e.g., parsed object, success indicator), leaving a gap in contextual completeness.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% with descriptions for both parameters. The description adds minimal value beyond the schema (only 'size limits' which mirrors maxLength). Baseline 3 is appropriate as the schema already does the work.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool parses JSON/XML and mentions size limits, distinguishing it from siblings like debug-calculator or debug-echo. However, it does not explicitly state what parsing means (e.g., returns structured data or validation).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is provided on when to use this parser versus other debug tools. There is no mention of prerequisites, typical use cases, or alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 7 tool updatesv0.0.18-security
    • First observeddebug-calculator
    • First observeddebug-database
    • First observeddebug-echo
    • First observeddebug-file-reader
    • First observeddebug-http
    • First observeddebug-image
    • First observeddebug-parser

TDQS

A3.5/5.0

Scored across 7 tools

Disambiguation5/5

Each tool has a distinct domain (calculator, database, echo, file reading, HTTP, image processing, parsing) with no functional overlap, making them easy to distinguish.

Naming Consistency5/5

All tools follow a consistent 'debug-<noun>' pattern with hyphens, providing a clear and predictable naming convention.

Tool Count5/5

Seven tools is well-scoped for a debugging/testing framework, covering essential operations without excess.

Completeness5/5

The tools cover a broad range of typical debugging scenarios (arithmetic, database, file I/O, HTTP, image processing, parsing) with safety restrictions, leaving no obvious gaps.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    A deliberately small MCP server that demonstrates security hardening against the OWASP MCP Top 10 with tools for file search, record queries, and document fetching, each defended against path traversal, SQL injection, and SSRF.
    4
    MIT
  • F
    license
    Not graded
    quality
    A
    maintenance
    A safety-first MCP server enabling file inspection/editing, allowlisted command execution, process listing, and optional desktop automation. It emphasizes security with disabled writes/commands/GUI by default and local feature gates.
    1
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables developers to scaffold secure-by-default MCP servers with deny-all permissions, typed tool stubs, and pre-wired audit hooks, while enforcing security posture through CI.
    1 npm
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables developers to build production-grade MCP servers with secure defaults, validated inputs, and CI security audits from the first commit.
    MIT