Join our Newsletter — 33% off our NHI Course
Home› Guides› Secure MCP Server Development: Input Validation, Path Scoping…
Guide MCP, Protocols & Agent Platforms

Secure MCP Server Development: Input Validation, Path Scoping and Safe Tool Handlers

PDF for newsletter subscribers · free to read online

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 13 November 2026 17 min read
On this page

Every argument that reaches an MCP tool handler was chosen by a language model that may have just read attacker-controlled text, so a tool handler must be written like an internet-facing API endpoint. The MCP specification puts this duty on the server: servers MUST validate all tool inputs, implement proper access controls, rate limit tool invocations and sanitise tool outputs. This guide is for the developers who write servers: how to design a narrow tool surface, validate arguments, confine file access to roots, run processes without a shell, handle outbound requests and return safe errors. It assumes you already know the protocol basics covered in MCP Security Best Practices Explained, and goes deeper into the code.

Key takeaways

  • The model is not a trusted caller. Prompt injection lets a third party steer tool arguments, so validate every argument in the handler as if it came from an anonymous HTTP request.
  • Narrow tools beat clever validation. A read_project_file tool with a constrained path is easier to secure than a run_command tool with a blocklist.
  • Canonicalise, then check, then open the checked object. Path scoping needs resolution of .. and symbolic links, a separator-aware containment check and protection against the file being swapped between check and use.
  • No shell, ever, for model-supplied values. Pass an argument array to a fixed executable, allow-list the values and end option parsing with -- where the command supports it.
  • Errors and outputs are a data channel too. Return validation failures as tool execution errors with isError: true, keep paths and secrets out of them, and never echo credentials back to the model.

Why the tool handler is the trust boundary

An MCP tool is model-controlled: the specification describes tools as something the language model can discover and invoke automatically. Client confirmation prompts are only a SHOULD in the specification. The arguments in a tools/call request are therefore shaped by everything in the model's context: the user's prompt, retrieved documents, web pages, issue comments and the outputs of other tools. Indirect prompt injection (MITRE ATLAS AML.T0051 LLM Prompt Injection, with ATLAS AML.T0053 AI Agent Tool Invocation describing the follow-on abuse of tools) turns any of those inputs into a way to choose your arguments.

Three consequences follow for server authors:

  • Client-side checks do not protect you: a malicious or misconfigured client can send any JSON it likes.
  • Annotations are hints, not controls: the specification says clients MUST consider tool annotations such as readOnlyHint and destructiveHint untrusted unless they come from trusted servers. Declaring a tool read-only does not make it read-only; your handler code does.
  • Your handler runs with your server's privileges: a local stdio server runs as the user, with their files and keys; a remote server holds service credentials. A handler bug reaches whatever that identity can reach.

OWASP's LLM Top 10 2025 names the two failure modes that meet here: LLM06 Excessive Agency (tools with more functionality, permission or autonomy than the task needs) and LLM05 Improper Output Handling (model output passed to a downstream system without validation). A tool handler that forwards a model-chosen string to a shell, a file API or a SQL query is LLM05 in its purest form.

Designing a narrow tool surface

The safest tool is one whose arguments cannot express anything dangerous.

  • One task per tool: prefer list_open_issues(repo, label) over github_api(method, path, body). Generic pass-through tools turn the model into an unrestricted API client with your token.
  • Identifiers, not locations: where possible, accept an identifier the server maps to a resource (a ticket number, a document ID, a report name from an enum) rather than a raw path, URL or query. CWE-22 calls this enforcement by conversion: map fixed input values to actual filenames and reject everything else.
  • Split read and write: separate tools let clients confirm, and operators disable, the destructive ones.

The inputSchema is the first, cheap layer. The specification requires it to be a valid JSON Schema object (JSON Schema 2020-12 when no $schema is given) and recommends {"type": "object", "additionalProperties": false} for tools with no parameters. Use the same strictness everywhere.

JSON Schema keywordUse in a tool schemaWhat it prevents
additionalProperties: falseOn every objectSmuggled parameters a handler might later read by accident
requiredList every argument the handler needsHandlers falling back to unsafe defaults when a field is missing
enumModes, report names, sort fields, environmentsFree text where a fixed set of values is enough
patternIdentifiers such as ^[A-Z]{2,10}-[0-9]{1,6}$Metacharacters, whitespace and path separators in IDs
maxLength, maxItems, maximumEvery string, array and numberResource exhaustion and oversized payloads to downstream systems
format (for example uri, date)Only as documentationNothing on its own: many validators treat format as an annotation unless format assertion is switched on

Declare an outputSchema where the result is structured. The specification says that if one is provided, servers MUST return structuredContent that conforms to it, which forces you to decide exactly which fields leave the server.

Validating arguments inside the handler

Schema validation tells you the shape is right. It cannot tell you that the ticket belongs to this user, that the date range is sane or that the branch name does not begin with a hyphen. Validate again in the handler, with these rules:

  1. Validate on the server, always: run your own schema validator on the received arguments. Do not assume the SDK or client did it.
  2. Decode and canonicalise before checking: URL-decode, Unicode-normalise and resolve paths first, then apply the allow-list to the canonical value. Checking the raw string and then using a decoded one is how double-encoding bypasses happen.
  3. Allow-list, do not blocklist: describe what is valid. A list of forbidden characters always misses one (a newline, a NUL byte, a Unicode lookalike).
  4. Authorise the object, not just the tool: a remote server that accepts document_id must check that the authenticated user can read that document. This is OWASP API1:2023 Broken Object Level Authorization applied to MCP.
  5. Fail closed with a useful message: return a tool execution error (isError: true) such as "label must be one of: bug, docs, security", so the model can correct itself, as the specification intends.

See the Input Validation and Output Encoding Guide for the general discipline; the rest of this guide covers the sinks that matter most in MCP servers.

Path scoping: confining file access to roots

File tools are the most common MCP server type and the most common source of handler bugs. The protocol offers roots: clients that declare the roots capability expose a list of file:// URIs, servers request them with roots/list, and clients send notifications/roots/list_changed when they change. The specification says servers SHOULD respect root boundaries and validate all paths against the provided roots. Roots are a statement of intent from the client; enforcement is entirely your code.

The reference filesystem server in the modelcontextprotocol/servers repository is a useful worked example. Its path validation, read for this guide in November 2026, follows this order:

  1. Reject NUL bytes, then normalise and resolve the requested path to an absolute path.
  2. Check containment against each allowed directory with a separator-aware comparison: the path must equal the directory or start with the directory plus a path separator, so a sibling such as /data/project-evil does not match /data/project.
  3. Resolve the real path (following symbolic links) and check containment again, refusing a symlink whose target is outside the allowed directories.
  4. For a file that does not exist yet, resolve and check the parent directory instead.
  5. When writing an existing file, refuse anything that is not a regular file, open it with O_NOFOLLOW on POSIX and confirm the opened handle is the same device and inode that was checked, so a swap between check and open is caught.
  6. On Windows, refuse alternate data stream names (a colon anywhere but the drive prefix), reserved device names such as CON, NUL and COM1, and components ending in a dot or space, because each names something other than what the containment check saw.
Path pitfallExample inputDefence
Dot-dot traversaldocs/../../.ssh/id_ed25519Resolve to an absolute canonical path before the containment check
Prefix match without separator/srv/app-secrets/key against root /srv/appCompare against root plus separator, or use a path-aware API such as Python Path.is_relative_to() on resolved paths
Symbolic link escapeA link inside the root pointing to /etcCheck the real path after link resolution (CWE-59)
Check-then-use raceDirectory swapped for a link after validationOpen with no-follow flags, verify the handle, operate on the handle (CWE-367)
Absolute path injection/home/user/.aws/credentialsJoin only after validation; never let an absolute argument replace the base
Windows name aliasingreport.txt::$DATA, NUL.txt, notes.txt.Refuse colons after the drive, device names and trailing dots or spaces

Two design rules sit above the code. Keep sensitive material out of the roots: a root at the user's home directory includes .ssh, .aws and browser profiles, which ATT&CK T1552.001 Credentials In Files describes as a standard collection target. If roots change at runtime, re-read them on notifications/roots/list_changed and treat an empty root list as "no access", not "full access". The reference server denies access when no allowed directories exist. See the Path Traversal Guide for the wider vulnerability class.

Running processes without a shell

Many MCP servers wrap a command-line tool: git, a linter, a cloud CLI, a database client. The OWASP OS Command Injection Defense Cheat Sheet gives the order of preference: avoid calling OS commands where a library can do the job; where you cannot, separate the command from its data with a structured API and validate both the command (against an allow-list) and its arguments. It also notes that every command injection is an argument injection.

LanguageUseAvoid for model-supplied valuesPlatform note
Pythonsubprocess.run(["git", "log", "--", path]) (shell is off by default)shell=True, os.system, string commandsPython docs: on Windows, .bat and .cmd files may run in a system shell regardless of arguments
Node.jsexecFile or spawn with an argument array (shell option default false)exec, execSync, any call with shell: trueNode docs: .bat and .cmd files cannot be launched with execFile; avoid wrapping them for model input
JavaProcessBuilder with the command and each argument separateRuntime.exec(String), which splits on whitespaceOWASP notes Runtime.exec(String) is deprecated since Java 18

Avoiding the shell stops ;, | and $() from being interpreted, but argument injection remains. A branch name of --upload-pack=... or a URL argument that begins with a hyphen is passed straight to the program as an option. Defend in three layers:

  • Fixed executable, fixed options: hard-code the binary path and every flag. The model supplies values only.
  • End option parsing: place -- before positional values where the program supports it, and reject values starting with - where it does not.
  • Constrain the environment: pass an explicit minimal environment, set a working directory inside the root, apply a timeout and cap output size.

A general "run command" tool belongs only in a sandbox with no credentials and restricted network, with a human approving each call. The Command Injection Guide covers the vulnerability class in depth.

Other sinks: URLs, queries and templates

  • Outbound requests: a fetch_url or webhook tool is an SSRF primitive. The MCP security best practices page lists the ranges clients should block during OAuth discovery (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, fc00::/7, fe80::/10) and the same list suits tool handlers. It also warns against hand-written IP parsing because of octal, hex and IPv4-mapped IPv6 encodings, recommends validating every redirect hop, and recommends an egress proxy for server-side deployments. See the SSRF Prevention Guide.
  • Database queries: parameterise values, allow-list identifiers such as column names, and give any query tool a read-only role scoped to the views it needs.
  • Templates, code and API paths: never pass model output to eval, template compilation or a deserialiser, and build downstream API paths only from validated identifiers.

Outputs, errors and rate limits

Tool results flow back into the model's context and often onto the user's screen. The specification requires servers to sanitise tool outputs. In practice that means:

  • Return only what the task needs: select fields explicitly; do not serialise whole database rows or API responses that contain tokens, internal IDs or personal data.
  • Label untrusted content: text fetched from the web, issues or email is attacker-influenced. Return it as data (for example in structuredContent fields) and avoid wording that reads as instructions from the server. The MCP Tool Poisoning Detection guide covers the related risk in tool descriptions.
  • Safe error text: distinguish protocol errors (JSON-RPC errors such as -32602 for an unknown tool or malformed request) from tool execution errors (isError: true). Put actionable, non-sensitive detail in the second; never include stack traces, absolute host paths, connection strings or tokens. CWE-22 itself advises keeping path details out of error messages.
  • Rate limits and quotas: the specification says servers MUST rate limit tool invocations. Limit per session and per user, and set separate, lower limits for expensive or destructive tools.

Where identity fits

  • Remote servers: the specification says MCP servers MUST NOT accept tokens that were not explicitly issued for the MCP server (no token passthrough), and MUST NOT use sessions for authentication. Validate the token audience on every request and call downstream APIs with credentials issued to your server, scoped per tool where the downstream system allows it. See the MCP Authorisation Specification Guide.
  • Local servers: prefer stdio so only the launching client can talk to the server. If you expose HTTP locally, the best practices page says to require an authorisation token or use a Unix domain socket or other restricted IPC. Read credentials from a secrets store or an environment variable set at launch, never from a file inside a root the model can read. See the Local MCP Server Security Guide.
  • Least privilege for the server's own credentials: a server that only reads issues should hold a token that can only read issues. Static, broad tokens in MCP configuration files are a recurring finding; see the MCP Static Token Audit.

How to implement: a phased approach

  1. Inventory tools and sinks (week 1): list every tool, its arguments and every sink each argument reaches (file system, process, network, database, template). Mark tools that change state.
  2. Tighten schemas: add additionalProperties: false, required, enum, pattern and length limits; add outputSchema for structured results.
  3. Centralise sink wrappers: write one path validator, one process runner and one outbound HTTP client, each enforcing the rules above, and make handlers call only those. Enforce this with a lint rule.
  4. Test hostile inputs: add unit tests for every row of the path pitfall table, for leading hyphens, newlines and NUL bytes, and for private IP and redirect cases. Fuzz tool arguments in CI. The MCP Security Testing Guide describes test harnesses and the MCP Security Scanners Guide covers automated checks.
  5. Pilot behind a flag: release the hardened handlers to an internal ring first and monitor validation rejections. Widen an allow-list deliberately when a legitimate use is rejected; never remove the check.
  6. Roll back safely: keep the previous release available, but roll back by disabling the affected tool (servers can change the tool list and send notifications/tools/list_changed) rather than by shipping the unvalidated handler again.

Decision guidance

  • Local stdio server for one developer: roots plus the path wrapper and no-shell rule are the minimum; sandbox the process if it runs any external command.
  • Remote multi-user server: add object-level authorisation on every identifier, per-user rate limits, audience-checked tokens and an egress proxy. Expect a security review before each new tool.
  • Wrapping an existing CLI: prefer its library or API; otherwise expose a few fixed subcommands.

Detection and measurement

The client SHOULD log tool usage, but the server is the only place that sees what each handler actually did. Log per call: tool name, caller identity, session, a hash or redacted form of the arguments, the resolved resource (canonical path, destination host, query identifier), the validation outcome and duration. Do not log raw secrets or full file contents; see the Sensitive Data in Logs Guide and the AI Agent Audit Log Schema Template.

Alert on containment failures, process arguments starting with -, outbound requests to blocked ranges and bursts of validation errors from one session, a common sign of injected instructions.

  • Tools with strict schemas: percentage of tools whose input schemas set additionalProperties: false and bound every string (target 100%).
  • Sink wrapper coverage: percentage of file, process and network calls in handler code routed through the shared wrappers (target 100%, enforced by lint).
  • Hostile input test pass rate: share of the path, argument and SSRF test cases passing in CI on every release.
  • Validation rejection rate: rejections per thousand calls per tool, trended; sudden rises warrant review.

Framework mapping

Control areaNIST SP 800-53 Rev 5NIST CSF 2.0Threat reference
Argument validation and allow-listsSI-10 Information Input ValidationPR.PS-06OWASP LLM05 Improper Output Handling
Output and error sanitisationSI-15 Information Output Filtering; SI-11 Error HandlingPR.DS-02OWASP LLM02 Sensitive Information Disclosure
Narrow tools and scoped credentialsAC-6 Least PrivilegePR.AA-05OWASP LLM06 Excessive Agency; ATLAS AML.T0053
Process sandboxingSC-39 Process IsolationPR.PS-01ATT&CK T1059 Command and Scripting Interpreter
Hostile input testingSA-11 Developer Testing and EvaluationPR.PS-06CWE-22, CWE-78
Tool call audit recordsAU-3 Content of Audit RecordsPR.PS-04ATLAS AML.T0051 (investigation of injected calls)

Common failure patterns

  • String-prefix containment: checking path.startsWith(root) without a trailing separator, which lets a sibling directory with a longer name pass.
  • Validating before link resolution: the normalised path is inside the root but a symbolic link inside it points elsewhere, and the handler opens the link.
  • Trusting values from configuration as well as arguments: paths read from a project file or manifest are just as attacker-controlled as tool arguments. In the Smithery.ai MCP Hosting Breach 2025, an unvalidated build path value of .. exposed a configuration file holding a platform token.
  • Shell for convenience: exec(`git log ${branch}`) written for a quick prototype and shipped. The fix is an argument array, not better escaping.
  • Verbose errors: returning exception text with absolute paths, SQL fragments or connection strings, which hands the model (and anyone steering it) a map of the host.

Practitioner checklist

  • Every tool does one task and accepts identifiers or constrained values rather than raw paths, URLs or commands where possible.
  • Every input schema sets additionalProperties: false, required and length or range limits; enums replace free text where the set is known.
  • Handlers re-validate arguments server-side and authorise each object against the caller.
  • All file access goes through one wrapper that resolves real paths, checks containment with a separator and guards against check-then-use races.
  • No handler invokes a shell; processes run from fixed executables with argument arrays, --, timeouts and a minimal environment.
  • Outbound requests block private, loopback and link-local ranges, validate redirects and go through an egress proxy on servers.
  • Tool execution errors use isError: true with actionable, non-sensitive messages; outputs return only needed fields.
  • Remote servers validate token audience, never pass client tokens downstream, and rate limit per user and per tool.
  • CI runs hostile-input tests for paths, arguments and SSRF on every release.
  • Tool calls are logged with caller, resolved resource and validation outcome, without secrets.

Standards and references

Related NHI Mgmt Group resources: MCP Security Guide · Remote MCP Server Hardening Guide · MCP Server Security Checklist · Top 10 MCP Security Risks

PDF for subscribers

Take this guide with you

Every section, table and checklist in one PDF, with links to the sources and related guides. Subscribe to our weekly NHI & AI Identity Journal and we’ll email you a download link.

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 13 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org

PDF Downloads for Newsletter Subscribers

Get the guide

Enter your details and we’ll email you a download link.

The full guide is free to read online. The PDF is a thank you for subscribing.

Fill in all fields marked * and tick the PDF box to continue.