πŸ€– HarDojo
Log in Sign up

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: true with 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: true signals 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.
";