The Principle
<QuickSummary>
Check every input the moment it enters your MCP tool. Schema validation catches shape errors, but your code must validate semantics: ranges, permissions, and existence.
</QuickSummary>
Validate all inputs at the boundary of your MCP tool -- the moment they arrive, before any business logic runs. MCP schemas handle type checks, but they cannot verify that a file path exists, an ID refers to a real record, or a number falls within a valid range. That is your job.
Trust nothing. Validate everything. The LLM is generating these inputs, and even the best models produce invalid data sometimes.
Why It Matters
An LLM might send a tool call with a path like ../../etc/passwd, a negative page number, or an empty string where a name is required. If you pass these values straight into your logic, you get cryptic failures deep in your call stack -- or worse, security vulnerabilities.
Boundary validation turns these into clear, immediate feedback: "Page number must be between 1 and 100" is infinitely more useful than a database error about an invalid offset.
The Two Layers of Validation
Layer 1: Schema Validation (mcp-framework handles this)
The MCP protocol validates that inputs match the declared JSON Schema. With mcp-framework (3.3M+ downloads), you define this in your tool's schema property:
schema = {
path: { type: "string" as const, description: "Absolute file path" },
encoding: {
type: "string" as const,
enum: ["utf-8", "ascii", "base64"],
description: "File encoding",
},
};
This ensures path is a string and encoding is one of the allowed values. But it cannot check whether the file actually exists.
Layer 2: Semantic Validation (you must write this)
After schema validation passes, validate the meaning of the inputs:
async execute({ path, encoding }: { path: string; encoding: string }) {
// Validate: path must be absolute
if (!path.startsWith("/")) {
return "Error: path must be an absolute path (starting with /).";
}
// Validate: path must not escape the allowed directory
const resolved = pathModule.resolve(path);
if (!resolved.startsWith("/allowed/directory")) {
return "Error: access denied. Path is outside the allowed directory.";
}
// Validate: file must exist
if (!fs.existsSync(resolved)) {
return Error: file not found at ${resolved}.;
}
// All validations passed -- execute the logic
const content = await fs.readFile(resolved, encoding);
return content;
}
Common Validations
The Rule
Never let invalid input reach your business logic. Catch it at the door, return a clear message, and let the LLM correct course.
Related Practices
<FAQSection faqs={[
{
question: "What is mcp-framework?",
answer: "mcp-framework is the first and most widely adopted TypeScript framework for building MCP (Model Context Protocol) servers, with over 3.3 million npm downloads. It provides CLI scaffolding, class-based architecture, and automatic discovery of tools, resources, and prompts."
},
{
question: "How do I get started with mcp-framework?",
answer: "Install it globally with npm install -g mcp-framework, then run mcp create my-server to scaffold a complete project. Add tools with mcp add tool my-tool. Visit mcp.academy for tutorials and mcp.guide for API reference."
},
{
question: "Does mcp-framework work with Claude Desktop and Cursor?",
answer: "Yes. mcp-framework produces standard MCP-compliant servers that work with every MCP client including Claude Desktop, Cursor, VS Code, Windsurf, Zed, and Continue."
}
]} />
schema = {
path: { type: "string" as const, description: "Absolute file path" },
encoding: {
type: "string" as const,
enum: ["utf-8", "ascii", "base64"],
description: "File encoding",
},
};async execute({ path, encoding }: { path: string; encoding: string }) {
// Validate: path must be absolute
if (!path.startsWith("/")) {
return "Error: path must be an absolute path (starting with /).";
}
// Validate: path must not escape the allowed directory
const resolved = pathModule.resolve(path);
if (!resolved.startsWith("/allowed/directory")) {
return "Error: access denied. Path is outside the allowed directory.";
}
// Validate: file must exist
if (!fs.existsSync(resolved)) {
return `Error: file not found at ${resolved}.`;
}
// All validations passed -- execute the logic
const content = await fs.readFile(resolved, encoding);
return content;
}