Skip to main content
Syed Noor

Building an MCP Server Integration with n8n

Step-by-step tutorial for connecting n8n AI agents to MCP servers — tool registration, schema definition, authentication, and production hardening.

Model Context Protocol (MCP) is the most significant infrastructure development for AI agents in 2026, and most automation teams have not wired it into their stack yet. If you are running n8n AI agent workflows — or planning to — this is the integration layer that turns your agents from isolated LLM callers into systems that can discover and use tools dynamically, across organizational boundaries, with standardized authentication and schema contracts.

I consult exclusively on n8n, so my bias is disclosed. I have built MCP integrations for client deployments where the alternative was hardcoding tool definitions into agent prompts — which works until you need to add, remove, or version a tool, at which point the entire agent configuration becomes a maintenance burden. MCP solves that problem architecturally.

This is a technical tutorial. It assumes you have n8n running (self-hosted or cloud), you are familiar with the AI Agent node, and you have at least basic comfort with JavaScript and JSON. If you need background on n8n’s AI agent capabilities first, read n8n AI Agent Workflows — Beyond Simple Automations.


What MCP Is and Why It Matters

Model Context Protocol is an open standard (originated at Anthropic, now adopted broadly) that defines how AI applications discover and invoke external tools. Think of it as the USB-C of AI tool integration: a standardized interface that lets any AI client connect to any tool server without custom wiring for each combination.

Before MCP, connecting an AI agent to external tools meant:

  1. Writing a tool description in the agent’s system prompt
  2. Defining the tool’s input schema manually
  3. Writing the execution logic to call the tool’s API
  4. Handling authentication, errors, and response parsing in custom code
  5. Repeating all of this for every tool, in every agent that uses it

With MCP:

  1. Tools are registered on an MCP server with standardized schemas
  2. AI clients (like n8n’s AI Agent node) discover available tools by querying the server
  3. Tool invocations follow a standardized request/response format
  4. Authentication, rate limiting, and access control are handled at the server level
  5. Adding a tool to an agent means pointing the agent at the server — no per-tool wiring

The analogy I use with clients: MCP is to AI tools what REST APIs were to web services. It replaces bespoke, fragile point-to-point integrations with a standardized protocol that any conforming client and server can use.

MCP Architecture in 30 Seconds

An MCP deployment has three components:

  • MCP Server — exposes tools with defined schemas. Can be a standalone service, a sidecar, or embedded in an existing API.
  • MCP Client — connects to one or more MCP servers, discovers tools, and invokes them on behalf of an AI agent. n8n’s MCP Tool node acts as a client.
  • Transport Layer — the communication channel between client and server. MCP supports two transports: stdio (for local/process-level communication) and SSE (Server-Sent Events) over HTTP (for networked communication). For n8n integrations, SSE over HTTP is the standard choice.

Building an MCP Server: Step by Step

We will build a practical MCP server that exposes three business tools — a customer lookup, an order status check, and a support ticket creator — then connect it to an n8n AI agent that can use all three dynamically.

Step 1: Set Up the MCP Server

The official MCP SDK is available for TypeScript and Python. We will use TypeScript since n8n’s ecosystem leans JavaScript/TypeScript.

Initialize the project:

mkdir mcp-business-tools && cd mcp-business-tools
npm init -y
npm install @modelcontextprotocol/sdk express
npm install -D typescript @types/node @types/express
npx tsc --init

Update tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  }
}

Step 2: Define Your Tools

Create src/server.ts. Each tool needs three things: a name, a description (the LLM reads this to decide when to use the tool), and a JSON Schema defining its input parameters.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import { z } from "zod";

const server = new McpServer({
  name: "business-tools",
  version: "1.0.0",
});

// Tool 1: Customer Lookup
server.tool(
  "lookup_customer",
  "Look up a customer by email address or customer ID. Returns customer " +
    "name, account status, subscription tier, and account creation date.",
  {
    identifier: z
      .string()
      .describe("Customer email address or customer ID (e.g., 'cust_abc123')"),
  },
  async ({ identifier }) => {
    // In production, this queries your actual customer database
    // For this tutorial, we simulate the lookup
    const customer = await lookupCustomer(identifier);
    if (!customer) {
      return {
        content: [
          {
            type: "text" as const,
            text: `No customer found for identifier: ${identifier}`,
          },
        ],
      };
    }
    return {
      content: [
        {
          type: "text" as const,
          text: JSON.stringify(customer, null, 2),
        },
      ],
    };
  }
);

// Tool 2: Order Status
server.tool(
  "check_order_status",
  "Check the current status of an order by order ID. Returns order status, " +
    "items, shipping tracking number, and estimated delivery date.",
  {
    order_id: z
      .string()
      .describe("The order ID (e.g., 'ord_xyz789')"),
  },
  async ({ order_id }) => {
    const order = await getOrderStatus(order_id);
    if (!order) {
      return {
        content: [
          {
            type: "text" as const,
            text: `No order found with ID: ${order_id}`,
          },
        ],
      };
    }
    return {
      content: [
        {
          type: "text" as const,
          text: JSON.stringify(order, null, 2),
        },
      ],
    };
  }
);

// Tool 3: Create Support Ticket
server.tool(
  "create_support_ticket",
  "Create a new support ticket for a customer. Use this when a customer " +
    "has an issue that needs to be tracked and resolved by the support team.",
  {
    customer_id: z
      .string()
      .describe("The customer ID to associate with the ticket"),
    subject: z
      .string()
      .describe("Brief summary of the issue (max 200 characters)"),
    description: z
      .string()
      .describe("Detailed description of the customer's issue"),
    priority: z
      .enum(["low", "medium", "high", "urgent"])
      .describe("Ticket priority level"),
  },
  async ({ customer_id, subject, description, priority }) => {
    const ticket = await createTicket({
      customer_id,
      subject,
      description,
      priority,
    });
    return {
      content: [
        {
          type: "text" as const,
          text: JSON.stringify(ticket, null, 2),
        },
      ],
    };
  }
);

Key design decisions in the tool definitions:

  • Descriptions are for the LLM. The agent reads the tool description to decide whether to invoke it. Write descriptions that explain when and why to use the tool, not just what it does. “Look up a customer by email address or customer ID” is better than “Customer lookup tool.”
  • Parameter descriptions guide the agent’s parameter extraction. If a user says “check on John’s order ORD-12345,” the agent needs the parameter description to understand that “ORD-12345” maps to order_id.
  • Use Zod schemas for validation. The MCP SDK integrates with Zod for runtime validation of incoming tool calls. Invalid parameters are rejected before your handler runs.

Step 3: Add the SSE Transport

Continue in src/server.ts — wire up the HTTP server with SSE transport:

const app = express();
const PORT = process.env.MCP_PORT || 3001;

// Store active transports for cleanup
const transports: Map<string, SSEServerTransport> = new Map();

// SSE endpoint — clients connect here to receive events
app.get("/sse", async (req, res) => {
  const transport = new SSEServerTransport("/messages", res);
  const sessionId = transport.sessionId;
  transports.set(sessionId, transport);

  res.on("close", () => {
    transports.delete(sessionId);
  });

  await server.connect(transport);
});

// Message endpoint — clients POST tool invocations here
app.post("/messages", async (req, res) => {
  const sessionId = req.query.sessionId as string;
  const transport = transports.get(sessionId);
  if (!transport) {
    res.status(400).json({ error: "Invalid session" });
    return;
  }
  await transport.handlePostMessage(req, res);
});

// Health check
app.get("/health", (req, res) => {
  res.json({
    status: "ok",
    tools: ["lookup_customer", "check_order_status", "create_support_ticket"],
    version: "1.0.0",
  });
});

app.listen(PORT, () => {
  console.log(`MCP server running on port ${PORT}`);
});

Step 4: Implement the Business Logic

Create src/handlers.ts for the actual tool implementations. In production, these call your real databases and APIs:

interface Customer {
  id: string;
  name: string;
  email: string;
  status: "active" | "inactive" | "suspended";
  tier: "free" | "pro" | "enterprise";
  created_at: string;
}

interface Order {
  id: string;
  customer_id: string;
  status: "pending" | "processing" | "shipped" | "delivered" | "cancelled";
  items: { name: string; quantity: number; price: number }[];
  tracking_number: string | null;
  estimated_delivery: string | null;
}

interface Ticket {
  id: string;
  customer_id: string;
  subject: string;
  description: string;
  priority: string;
  status: "open";
  created_at: string;
}

export async function lookupCustomer(
  identifier: string
): Promise<Customer | null> {
  // Replace with your actual database query
  // Example: const result = await db.query(
  //   'SELECT * FROM customers WHERE email = $1 OR id = $1',
  //   [identifier]
  // );
  return {
    id: "cust_abc123",
    name: "Jane Smith",
    email: "jane@example.com",
    status: "active",
    tier: "pro",
    created_at: "2025-03-15T10:00:00Z",
  };
}

export async function getOrderStatus(
  orderId: string
): Promise<Order | null> {
  // Replace with your actual order system query
  return {
    id: orderId,
    customer_id: "cust_abc123",
    status: "shipped",
    items: [
      { name: "Widget Pro", quantity: 2, price: 49.99 },
      { name: "Widget Mount", quantity: 1, price: 19.99 },
    ],
    tracking_number: "1Z999AA10123456784",
    estimated_delivery: "2026-05-30",
  };
}

export async function createTicket(params: {
  customer_id: string;
  subject: string;
  description: string;
  priority: string;
}): Promise<Ticket> {
  // Replace with your actual ticketing system API call
  // Example: const ticket = await zendesk.tickets.create({...});
  return {
    id: `tkt_${Date.now()}`,
    customer_id: params.customer_id,
    subject: params.subject,
    description: params.description,
    priority: params.priority,
    status: "open",
    created_at: new Date().toISOString(),
  };
}

Build and run:

npx tsc
node dist/server.js

Test the health endpoint to verify:

curl http://localhost:3001/health

You should see the three tools listed in the response.


Connecting the MCP Server to n8n

Step 5: Configure the n8n AI Agent Workflow

In your n8n instance, create a new workflow with this structure:

Trigger node: Chat Trigger (for testing) or Webhook (for production).

AI Agent node: This is the orchestrator. Configure it:

  • Agent Type: Tools Agent
  • Model: Your preferred LLM (Claude 3.5 Sonnet or GPT-4o recommended for reliable tool calling)
  • System Prompt:
You are a customer support agent. You have access to tools for looking up
customer information, checking order status, and creating support tickets.

When a customer contacts you:
1. First, look up their customer record using their email or customer ID
2. If they ask about an order, check the order status
3. If they have an issue that needs tracking, create a support ticket
4. Always confirm actions with the customer before creating tickets

Be concise and professional. If you cannot find information, say so clearly
rather than guessing.

MCP Tool node: This is where the connection happens. n8n’s MCP Tool node (available since n8n 1.64+) connects to an MCP server and automatically discovers all available tools.

  • Connection Type: SSE
  • Server URL: http://your-mcp-server:3001/sse

When you save the MCP Tool node configuration, n8n queries the MCP server’s tool list and displays the discovered tools (lookup_customer, check_order_status, create_support_ticket) in the node panel. The AI Agent node can now invoke any of them.

Connect the nodes: Chat Trigger -> AI Agent. Attach the MCP Tool node to the AI Agent’s tool input.

Step 6: Test the Integration

Open the Chat panel in n8n and test with a realistic customer message:

Hi, my name is Jane and my email is jane@example.com. 
I ordered two Widget Pros last week (order ORD-12345) and 
they have not arrived yet. Can you check the status?

The AI Agent should:

  1. Call lookup_customer with jane@example.com to verify the customer
  2. Call check_order_status with ORD-12345 to get shipping details
  3. Respond with the tracking number and estimated delivery date

Watch the execution log — you should see two tool calls in the agent’s execution path, with the MCP server’s responses flowing back into the agent’s reasoning loop.


Production Hardening

The tutorial above works for development. Deploying this to production requires additional layers.

Authentication

Your MCP server should not be open to any client. Add API key authentication:

// Middleware for MCP endpoints
const authenticateMCP = (
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) => {
  const apiKey = req.headers["x-api-key"];
  if (apiKey !== process.env.MCP_API_KEY) {
    res.status(401).json({ error: "Unauthorized" });
    return;
  }
  next();
};

app.get("/sse", authenticateMCP, async (req, res) => {
  // ... existing SSE handler
});

app.post("/messages", authenticateMCP, async (req, res) => {
  // ... existing message handler
});

In n8n, add the API key as a header credential on the MCP Tool node’s HTTP configuration. Store the key in n8n’s credential manager, not hardcoded in the workflow.

Rate Limiting

AI agents can invoke tools in rapid loops. Without rate limiting, a misconfigured agent can hammer your MCP server — and the downstream APIs it calls — with hundreds of requests per minute.

import rateLimit from "express-rate-limit";

const mcpLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 60, // 60 requests per minute per IP
  message: { error: "Rate limit exceeded. Max 60 requests per minute." },
});

app.use("/sse", mcpLimiter);
app.use("/messages", mcpLimiter);

Also configure n8n’s AI Agent node with a max iterations setting (I recommend 5-10 for most use cases). This prevents the agent from entering an infinite tool-calling loop if it cannot resolve its goal.

Error Handling

Tool handlers should never throw unhandled exceptions. Wrap every handler in try-catch and return structured error responses:

server.tool(
  "lookup_customer",
  "Look up a customer by email or ID.",
  { identifier: z.string() },
  async ({ identifier }) => {
    try {
      const customer = await lookupCustomer(identifier);
      if (!customer) {
        return {
          content: [{
            type: "text" as const,
            text: `No customer found for: ${identifier}`,
          }],
          isError: false,
        };
      }
      return {
        content: [{
          type: "text" as const,
          text: JSON.stringify(customer, null, 2),
        }],
      };
    } catch (error) {
      return {
        content: [{
          type: "text" as const,
          text: `Error looking up customer: ${error.message}`,
        }],
        isError: true,
      };
    }
  }
);

The isError: true flag tells the AI agent that the tool call failed, so it can decide whether to retry, try a different approach, or inform the user — rather than trying to parse an error stack trace as a customer record.

Logging and Observability

Every tool invocation should be logged with:

  • Timestamp
  • Tool name
  • Input parameters (sanitize PII if applicable)
  • Response status (success/error)
  • Latency
  • The session ID (to correlate multi-tool agent runs)
server.tool("lookup_customer", "...", { identifier: z.string() },
  async ({ identifier }) => {
    const start = Date.now();
    try {
      const result = await lookupCustomer(identifier);
      console.log(JSON.stringify({
        tool: "lookup_customer",
        input: { identifier },
        status: "success",
        latency_ms: Date.now() - start,
        timestamp: new Date().toISOString(),
      }));
      return { content: [{ type: "text" as const, text: JSON.stringify(result) }] };
    } catch (error) {
      console.log(JSON.stringify({
        tool: "lookup_customer",
        input: { identifier },
        status: "error",
        error: error.message,
        latency_ms: Date.now() - start,
        timestamp: new Date().toISOString(),
      }));
      return {
        content: [{ type: "text" as const, text: `Error: ${error.message}` }],
        isError: true,
      };
    }
  }
);

Forward these logs to your centralized logging stack (ELK, Loki, CloudWatch) alongside n8n’s execution logs. This gives you a complete audit trail: the n8n workflow triggered, the agent decided to call tool X, the MCP server processed the call, the downstream API responded, and the agent synthesized the result.

Tool Versioning

When you update a tool’s schema (adding a parameter, changing a response format), existing agents may break. MCP handles this through server versioning:

const server = new McpServer({
  name: "business-tools",
  version: "1.1.0",  // Bump on schema changes
});

For non-breaking changes (new optional parameters, additional response fields), bump the minor version. For breaking changes (removed parameters, changed types), bump the major version and run the old and new servers in parallel during migration.


Advanced Pattern: Multi-Server Agent

One of MCP’s architectural strengths is that an agent can connect to multiple MCP servers simultaneously. In n8n, you attach multiple MCP Tool nodes to the same AI Agent node, each pointing to a different server.

Example architecture:

  • MCP Server 1: Customer data tools (lookup, update, history)
  • MCP Server 2: Order management tools (status, refund, shipping)
  • MCP Server 3: Internal knowledge base (documentation search, policy lookup)

The AI Agent discovers tools from all three servers and selects the appropriate one for each step of its reasoning. The servers can be owned by different teams, deployed independently, and versioned on their own schedules. This is the microservices pattern applied to AI tool infrastructure.

In n8n, this looks like three MCP Tool nodes connected to one AI Agent node. The agent sees a unified tool list and invokes tools across servers seamlessly. The separation is invisible to the agent — which is exactly the point.


When MCP Is Overkill

Not every n8n AI workflow needs MCP. Use MCP when:

  • You have 5+ tools that the agent needs to access
  • Tools are maintained by different teams or evolve on different schedules
  • You need standardized authentication and access control across tool providers
  • You want to share tools across multiple agents without duplicating configuration
  • Tool schemas change frequently and you need dynamic discovery

For simpler cases — an agent with 2-3 tools that you control and that rarely change — n8n’s built-in tool nodes (HTTP Request, Code, Database) are simpler and sufficient. MCP adds architectural value at the cost of an additional service to deploy and maintain. Use it when the value exceeds the cost.


What to Do Next

If you are building AI agent workflows on n8n and want production patterns beyond what this tutorial covers — multi-agent orchestration, RAG pipelines, cost optimization, human-in-the-loop gates — read n8n AI Agent Workflows — Beyond Simple Automations. It covers the four architecture patterns that handle 90% of production AI workflows.

If you want your AI agent workflows built with production-readiness from day one — error handling, cost controls, monitoring, and the MCP integrations described in this post — the noorflows AI Agent Build ($2,497) delivers a production-grade agent system in 10 business days, including MCP server setup, tool registration, and the n8n workflow wiring.

If you already have n8n running and want to know whether your existing workflows are production-ready, the Pre-flight Audit ($247) scores your setup against the 6-Dimension Production-Readiness Checklist and delivers a prioritized report within 24-72 hours.

For industry-specific AI agent patterns, see what we build for SaaS operations, e-commerce teams, and digital agencies.

Or email me directly with your use case. I will tell you whether MCP is the right architecture for your situation or whether a simpler approach gets you there faster.

Get in touch