ity solution will remain vulnerable to output-based prompt injection and credential misuse. The optimal posture combines static validation with explicit permission scoping, version pinning, and output sanitization.
Core Solution
Securing an MCP toolchain requires architectural decisions that align with how LLMs parse tool definitions and how clients manage credential boundaries. The following implementation demonstrates a hardened TypeScript MCP server setup, client configuration, and validation pipeline.
1. Server Initialization & Version Pinning
MCP servers should never reference mutable tags in production. Pinning to a specific commit or semantic version prevents behavioral drift between agent sessions.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// Pin to exact release hash or semantic version
const SERVER_VERSION = "v1.4.2";
const SERVER_PROVENANCE = "github:acme-internal/secure-audit-tool";
const auditServer = new McpServer({
name: "secure-audit-tool",
version: SERVER_VERSION,
capabilities: {
tools: true,
resources: false, // Explicitly disable unused capabilities
prompts: false
}
});
// Register transport with explicit error boundaries
const transport = new StdioTransport();
await auditServer.connect(transport);
Rationale: Explicit capability declaration prevents accidental exposure of unused interfaces. Version pinning ensures the agent always interacts with the exact artifact that passed security validation.
Tool descriptions must be treated as executable context. Descriptions should contain only declarative documentation, never imperative instructions or conditional logic.
import { z } from "zod";
// Safe metadata pattern: declarative only, no imperative verbs
const describeRepoAudit = {
name: "analyze_repository_structure",
description: "Returns a hierarchical list of directories and files matching the provided glob pattern. Output is formatted as JSON.",
parameters: z.object({
target_path: z.string().describe("Absolute path to the repository root"),
file_pattern: z.string().default("*.ts").describe("Glob pattern for file filtering")
})
};
auditServer.tool(
describeRepoAudit.name,
describeRepoAudit.description,
describeRepoAudit.parameters.shape,
async (args) => {
// Implementation strictly follows declared behavior
const { target_path, file_pattern } = args;
const results = await scanDirectory(target_path, file_pattern);
return { content: [{ type: "text", text: JSON.stringify(results) }] };
}
);
Rationale: LLMs interpret tool descriptions as execution instructions. Removing imperative phrasing ("before running this, check...", "always output to...") eliminates metadata injection vectors. Zod schemas enforce parameter types at the protocol level, preventing type coercion attacks.
3. Permission Scoping & Token Isolation
Credentials should never be shared across servers. Each MCP server must receive narrowly scoped tokens with explicit revocation paths.
// Client-side credential vaulting pattern
const credentialVault = new Map<string, ScopedToken>();
function registerServerCredentials(serverId: string, token: ScopedToken) {
// Enforce least privilege at registration
if (token.scopes.length > 3) {
throw new Error("Token scope exceeds maximum allowed permissions");
}
credentialVault.set(serverId, token);
}
// Example: GitHub-specific server token
registerServerCredentials("secure-audit-tool", {
value: process.env.GH_REPO_READ_TOKEN,
scopes: ["repo:status", "public_repo"],
expiresAt: new Date(Date.now() + 3600000) // 1-hour TTL
});
Rationale: Token isolation limits blast radius. If a server is compromised, only its specific credential is exposed. Short TTLs and explicit scope lists prevent privilege escalation.
4. Integration with Scanning Pipeline
Static scanning should run as a pre-commit or CI gate before server configuration reaches developer environments.
# .github/workflows/mcp-security-check.yml
name: MCP Server Validation
on:
pull_request:
paths: ["mcp-config/**"]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run MCP Security Scan
uses: github/mcp-scanner-action@v1
with:
config-path: "./mcp-config/servers.json"
fail-on: "high,critical"
check-provenance: true
check-metadata: true
Rationale: Automating validation ensures no configuration bypasses security checks. Failing on high/critical findings prevents accidental deployment of unsanitized servers.
Pitfall Guide
Explanation: Developers assume tool descriptions are purely informational. LLMs parse them as execution context, making imperative phrasing a direct injection vector.
Fix: Enforce declarative-only descriptions. Use linters to flag imperative verbs, conditional logic, or hidden Unicode characters in metadata.
Explanation: Referencing latest or main branches allows server authors to push behavioral changes without agent awareness. This enables rug-pull attacks where benign servers become malicious post-approval.
Fix: Pin all servers to exact semantic versions or commit hashes. Implement automated drift detection that alerts when a server's advertised tools change.
3. Over-Provisioning Agent Credentials
Explanation: Sharing a single personal access token across multiple MCP servers grants every server full account privileges. Compromise of one server compromises all.
Fix: Generate dedicated, narrowly scoped tokens per server. Use short-lived credentials with automatic rotation. Store tokens in a vault, never in environment variables or config files.
4. Assuming Static Scanning Covers Runtime Outputs
Explanation: GitHub's scanning validates server code and metadata before connection. It cannot inspect data returned by tools during runtime. A clean server can relay prompt injection payloads from databases, APIs, or repositories.
Fix: Implement output sanitization at the client level. Strip or escape executable instructions from tool responses before passing them to the LLM. Treat all tool outputs as untrusted input.
5. Ignoring Transitive Dependency Audits
Explanation: MCP servers install like any other package. Compromised dependencies execute within the agent's context, inheriting granted permissions and filesystem access.
Fix: Run dependency vulnerability scans (npm audit, pip-audit, trivy) as part of the server validation pipeline. Pin dependency versions and use lockfiles. Prefer servers with minimal dependency trees.
6. Bypassing Client-Side Permission Toggles
Explanation: Clients like Cursor and Claude Desktop provide explicit enable/disable toggles and permission scopes for connected servers. Developers often click through these panels without reviewing granted capabilities.
Fix: Treat client configuration panels as security checkpoints. Disable servers by default. Enable only after verifying tool lists, permission requests, and provenance. Audit enabled servers quarterly.
7. Failing to Re-Validate After Updates
Explanation: Server updates can introduce new tools, modify existing behavior, or expand permission requirements. Agents automatically inherit these changes without developer review.
Fix: Implement update gating. When a server version changes, pause agent execution, trigger a fresh security scan, and require explicit developer approval before resuming.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal toolchain with controlled authorship | Static scanning + version pinning + scoped tokens | Low risk of malicious updates; scanning catches accidental misconfigurations | Low |
| Public marketplace servers | Scanning + strict permission scoping + output sanitization + update gating | High risk of supply chain attacks and behavioral drift; runtime validation required | Medium |
| CI/CD pipeline integration | Scanning + automated drift detection + credential vaulting + capability negotiation | Unattended execution requires maximum isolation and auditability | High |
| Local development sandbox | Manual configuration + client-side toggles + disposable credentials | Speed prioritized over security; isolated environment limits blast radius | Low |
Configuration Template
{
"mcpServers": {
"secure-audit-tool": {
"command": "npx",
"args": ["@acme/audit-server@1.4.2"],
"env": {
"MCP_SERVER_ID": "secure-audit-tool",
"CREDENTIAL_VAULT_PATH": "/var/run/secrets/audit-token"
},
"permissions": {
"filesystem": ["read:/repos/acme-internal"],
"network": ["https://api.github.com"],
"shell": false
},
"validation": {
"pinVersion": true,
"scanMetadata": true,
"checkProvenance": true,
"revalidateOnUpdate": true
}
}
}
}
Quick Start Guide
- Initialize Server Configuration: Create a JSON manifest referencing the exact server version. Define explicit permission scopes and disable unused capabilities.
- Generate Scoped Credentials: Create a dedicated token with minimal required permissions. Store it in a secure vault and reference it via environment variables or secret managers.
- Enable Client Validation: Open your AI agent's MCP settings panel. Verify the server appears in the enabled list. Confirm tool descriptions match expected behavior.
- Run Pre-Connection Scan: Execute GitHub's MCP security scanner or equivalent static analysis tool against your configuration. Resolve any high/critical findings before proceeding.
- Test in Isolated Environment: Connect the server to a sandboxed agent instance. Verify tool outputs are sanitized and permissions align with declared scopes. Promote to production only after validation passes.