Deploy MCP in Production
Security, monitoring, multi-server setups, and real-world patterns.
Building a server is step one. This project covers making it production-ready: security hardening, error handling, monitoring, and connecting multiple servers to one client.
Step 1: Add error handling
Every tool should catch errors and return them gracefully:
server.tool("dangerous_operation", "...", { input: z.string() }, async ({ input }) => {
try {
const result = await doSomething(input);
return { content: [{ type: "text", text: result }] };
} catch (err) {
return {
content: [{ type: "text", text: `Error: ${err.message}` }],
isError: true, // Signal to the client that this failed
};
}
});
The isError: true flag tells the client the tool failed, so the model can adjust its plan.
Step 2: Add input validation
Never trust the model's output. Always validate:
server.tool("query_db", "Run a read-only SQL query", {
sql: z.string().refine(
(s) => /^\s*(SELECT|WITH)\b/i.test(s),
"Only SELECT queries are allowed"
)
}, async ({ sql }) => {
// The refine check already rejected non-SELECT queries
const result = await db.query(sql);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
});
Step 3: Add environment-based secrets
Never hardcode secrets:
const token = process.env.GITHUB_TOKEN;
if (!token) {
console.error("GITHUB_TOKEN not set β GitHub tools will be unavailable");
}
And in your client config:
{
"mcpServers": {
"github": {
"command": "node",
"args": ["server.js"],
"env": { "GITHUB_TOKEN": "ghp_..." }
}
}
}
Step 4: Multi-server setup
Connect multiple MCP servers to one client:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "..." }
},
"todo": {
"command": "node",
"args": ["/tools/todo-mcp/server.js"]
}
}
}
Each server provides different tools. The client merges them into one unified set the model can use.
Step 5: Monitoring
Log tool calls for debugging:
server.tool("tracked_op", "...", schema, async (args) => {
const start = Date.now();
console.log(`[tool] tracked_op called with`, args);
try {
const result = await operate(args);
console.log(`[tool] tracked_op completed in ${Date.now() - start}ms`);
return { content: [{ type: "text", text: result }] };
} catch (err) {
console.error(`[tool] tracked_op FAILED: ${err.message}`);
throw err;
}
});
Step 6: Checklist before shipping
- [ ] Every tool validates its inputs.
- [ ] Secrets come from environment variables.
- [ ] Errors return
isError: truewith helpful messages. - [ ] Tool descriptions are clear enough for the model to use correctly.
- [ ] No tool has write access to unexpected paths.
- [ ] The server starts and stops cleanly.
What you learned
- Production MCP servers need error handling, input validation, and secure secret management.
isError: truesignals failures to the client model.- Multi-server setups let you compose capabilities from multiple tools.
- Logging tool calls is essential for debugging and monitoring.
- Security: always validate what the model asks you to do β it can make mistakes.