Documentation

MCP TypeScript Security Checks

These checks are specific to Model Context Protocol (MCP) servers written in TypeScript or JavaScript. They identify common security issues and best practice violations in MCP server implementations.

mcp-excessive-tools

Severity: Medium

What It Detects

MCP servers that expose more than 10 tools.

Why It's Concerning

Many tools:

  • Increase attack surface
  • Make security auditing harder
  • May indicate overly broad capabilities
  • Could expose unintended functionality

Example Finding

// Server with many tools
server.tool('readFile', ...);
server.tool('writeFile', ...);
server.tool('deleteFile', ...);
server.tool('execute', ...);
// ... 20+ more tools

How to Address

  • Split into focused MCP servers
  • Remove unused tools
  • Consider if all tools are necessary
  • Document why many tools are needed

mcp-dangerous-tool

Severity: High

What It Detects

Tools with dangerous-sounding names:

  • exec, execute, run
  • delete, remove, drop
  • admin, sudo, root
  • shell, bash, cmd

Why It's Concerning

Tools with these names often:

  • Execute arbitrary commands
  • Delete data destructively
  • Have elevated privileges
  • Bypass security controls

Example Finding

server.tool('executeCommand', async ({ command }) => {
  // Executes arbitrary shell commands
  return await exec(command);
});

How to Address

// Instead of generic "execute"
// Create specific, limited tools
 
server.tool('runBuild', async () => {
  // Fixed command, no user input
  return await exec('npm run build');
});
 
server.tool('listFiles', async ({ directory }) => {
  // Validate and limit scope
  const safePath = validatePath(directory);
  return await fs.readdir(safePath);
});

mcp-missing-description

Severity: Low

What It Detects

MCP tools registered without descriptions.

Why It's Important

Tool descriptions:

  • Help AI models use tools correctly
  • Provide transparency to users
  • Document intended behavior
  • Enable better error messages

Example Finding

// Tool without description
server.tool('processData', async ({ data }) => {
  return process(data);
});

How to Fix

server.tool('processData', {
  description: 'Processes JSON data and returns formatted output',
  inputSchema: {
    type: 'object',
    properties: {
      data: {
        type: 'string',
        description: 'JSON string to process'
      }
    },
    required: ['data']
  }
}, async ({ data }) => {
  return process(data);
});

mcp-unbounded-operation

Severity: Medium

What It Detects

Operations without limits:

  • fetch() without timeout
  • Infinite loops
  • Unbounded recursion

Why It's Dangerous

Unbounded operations can:

  • Hang indefinitely
  • Consume all resources
  • Create denial of service
  • Make debugging difficult

Example Finding

server.tool('fetchData', async ({ url }) => {
  // No timeout - could hang forever
  const response = await fetch(url);
  return response.text();
});

How to Fix

server.tool('fetchData', async ({ url }) => {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout
 
  try {
    const response = await fetch(url, { signal: controller.signal });
    return response.text();
  } finally {
    clearTimeout(timeout);
  }
});

mcp-missing-validation

Severity: Medium

What It Detects

Tool handlers that don't validate input:

  • No type checking
  • No schema validation
  • Direct use of input without verification

Why It's Important

Missing validation can:

  • Allow injection attacks
  • Cause unexpected errors
  • Lead to data corruption
  • Enable privilege escalation

Example Finding

server.tool('writeFile', async ({ path, content }) => {
  // No validation of path or content
  await fs.writeFile(path, content);
});

How to Fix

import { z } from 'zod';
 
const writeFileSchema = z.object({
  path: z.string()
    .max(255)
    .refine(p => !p.includes('..'), 'Path traversal not allowed'),
  content: z.string().max(1000000) // 1MB limit
});
 
server.tool('writeFile', {
  inputSchema: writeFileSchema
}, async (input) => {
  const { path, content } = writeFileSchema.parse(input);
  const safePath = validateAndResolvePath(path);
  await fs.writeFile(safePath, content);
});

mcp-sensitive-exposure

Severity: High

What It Detects

Potential exposure of sensitive data in tool responses:

  • Returning credentials
  • Exposing internal paths
  • Leaking configuration

Why It's Dangerous

Exposed data can:

  • Leak to AI logs
  • Be visible to end users
  • Enable further attacks
  • Violate privacy regulations

Example Finding

server.tool('getConfig', async () => {
  return {
    apiKey: process.env.API_KEY, // Exposes credential!
    dbPassword: config.database.password // Exposes password!
  };
});

How to Fix

server.tool('getConfig', async () => {
  return {
    // Only expose non-sensitive config
    apiEndpoint: config.apiEndpoint,
    timeout: config.timeout,
    // Indicate credentials are set without exposing them
    hasApiKey: !!process.env.API_KEY,
    hasDatabaseConfig: !!config.database?.host
  };
});

Summary

| Check | Severity | Key Fix | |-------|----------|---------| | mcp-excessive-tools | Medium | Split into focused servers | | mcp-dangerous-tool | High | Create specific, limited tools | | mcp-missing-description | Low | Add descriptions to all tools | | mcp-unbounded-operation | Medium | Add timeouts and limits | | mcp-missing-validation | Medium | Validate all inputs | | mcp-sensitive-exposure | High | Never expose credentials in responses |

MCP Security Best Practices

1. Principle of Least Privilege

Only expose tools that are absolutely necessary:

// Bad: Generic file access
server.tool('readFile', ...);
server.tool('writeFile', ...);
 
// Good: Specific, limited operations
server.tool('readConfig', ...);  // Only reads specific config file
server.tool('saveUserPrefs', ...);  // Only saves to user directory

2. Validate All Input

import { z } from 'zod';
 
const schema = z.object({
  filename: z.string().max(100),
  options: z.object({
    format: z.enum(['json', 'yaml', 'text'])
  }).optional()
});

3. Sanitize All Output

// Remove sensitive fields before returning
const sanitize = (data) => {
  const { password, apiKey, ...safe } = data;
  return safe;
};

4. Add Timeouts

const withTimeout = (promise, ms) => {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Timeout')), ms)
    )
  ]);
};

Next Steps