Skip to content
Blog

Building an Agentic Support System End-to-End

Build a complete agentic customer support system using MCP servers, tool orchestration, and multi-step reasoning. From architecture to production deployment.

Published on September 7, 2026

AI Assistant

Customer support is the killer use case for AI agents. It has structured workflows, clear success metrics, and high volume — exactly the conditions where agentic systems outperform humans at scale. But building a support agent that actually works — one that can look up orders, process refunds, answer policy questions, and escalate appropriately — requires more than a chatbot with API calls bolted on.

In this post, we build an end-to-end agentic support system using the Model Context Protocol (MCP) to connect an LLM to your support tools: order databases, knowledge bases, ticketing systems, and escalation workflows.

Why This Matters

Traditional support chatbots are glorified FAQ lookup engines. They match user questions to predefined answers and fail the moment a question requires combining information from multiple systems. An agentic support system is fundamentally different:

  • It reasons about which tools to use based on the user’s intent
  • It chains multiple tool calls together to resolve complex issues
  • It knows when to escalate to a human instead of guessing
  • It learns from each interaction to improve future responses

The Model Context Protocol (MCP) provides the standard interface for connecting AI applications to these external systems — exposing tools, resources, and prompts that the agent can discover and use at runtime.

Architecture Overview

Our system has four layers:

  1. MCP Servers — Expose internal tools and data as standardized MCP primitives
  2. Agent Core — Orchestrates tool selection and multi-step reasoning
  3. Knowledge Layer — RAG pipeline for policies, FAQs, and documentation
  4. Human Escalation — Thresholds and workflows for when AI confidence is low
User Request

Agent (Orchestrator)
    ├── MCP Server: Order Service (tools: get_order, refund_order, track_shipment)
    ├── MCP Server: Knowledge Base (resources: policies, FAQs)
    ├── MCP Server: Ticketing System (tools: create_ticket, update_ticket)
    └── Escalation Module (confidence < threshold → human handoff)

Step 1: Build the Order Service MCP Server

First, we create an MCP server that exposes order management operations:

from mcp.server import MCPServer
import httpx

mcp = MCPServer("order-service")

ORDER_API_BASE = "https://api.yourcompany.com/orders"

@mcp.tool()
async def get_order(order_id: str) -> str:
    """Retrieve order details by order ID.

    Args:
        order_id: The unique order identifier (e.g., ORD-12345)
    """
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"{ORDER_API_BASE}/{order_id}",
            headers={"Authorization": "Bearer $API_KEY"},
        )
        if response.status_code == 404:
            return f"Order {order_id} not found."
        order = response.json()
        return (
            f"Order {order['id']}: Status={order['status']}, "
            f"Total=${order['total']}, Items={len(order['items'])}, "
            f"Placed={order['created_at']}"
        )

@mcp.tool()
async def process_refund(order_id: str, reason: str) -> str:
    """Process a refund for an order.

    Args:
        order_id: The order to refund
        reason: Reason for the refund (required for audit)
    """
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{ORDER_API_BASE}/{order_id}/refund",
            json={"reason": reason},
            headers={"Authorization": "Bearer $API_KEY"},
        )
        result = response.json()
        if result.get("success"):
            return f"Refund of ${result['amount']} processed for order {order_id}."
        return f"Refund failed: {result.get('error', 'Unknown error')}"

@mcp.tool()
async def track_shipment(order_id: str) -> str:
    """Track the current shipment status for an order.

    Args:
        order_id: The order to track
    """
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"{ORDER_API_BASE}/{order_id}/shipment",
            headers={"Authorization": "Bearer $API_KEY"},
        )
        shipment = response.json()
        return (
            f"Shipment for {order_id}: Carrier={shipment['carrier']}, "
            f"Status={shipment['status']}, "
            f"ETA={shipment.get('eta', 'Unknown')}"
        )

if __name__ == "__main__":
    mcp.run(transport="stdio")

Step 2: Build the Knowledge Base MCP Server

This server exposes company policies and FAQs as MCP resources:

from mcp.server import MCPServer

mcp = MCPServer("knowledge-base")

POLICIES = {
    "refund_policy": """
    Refund Policy: Full refund within 30 days of purchase. Items must be
    in original condition. Shipping costs are non-refundable. Digital
    products are non-refundable after download.
    """,
    "shipping_policy": """
    Shipping Policy: Standard shipping 5-7 business days. Express shipping
    2-3 business days ($9.99). Free shipping on orders over $50.
    International shipping available to 40+ countries.
    """,
    "return_policy": """
    Return Policy: Returns accepted within 30 days. Customer pays return
    shipping. Items must be unused and in original packaging. Exchanges
    available for different sizes/colors.
    """,
}

@mcp.resource("policy://{policy_name}")
async def get_policy(policy_name: str) -> str:
    """Retrieve a specific company policy by name."""
    if policy_name in POLICIES:
        return POLICIES[policy_name]
    return f"Policy '{policy_name}' not found. Available: {list(POLICIES.keys())}"

@mcp.resource("knowledge://search")
async def search_knowledge(query: str) -> str:
    """Search the knowledge base for relevant information."""
    # In production, this would use a vector search
    results = []
    for name, content in POLICIES.items():
        if query.lower() in content.lower():
            results.append(f"{name}: {content[:200]}...")
    return "\n".join(results) if results else "No relevant information found."

if __name__ == "__main__":
    mcp.run(transport="stdio")

Step 3: Build the Orchestrator Agent

The agent connects to MCP servers and reasons about which tools to use:

import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

class SupportAgent:
    def __init__(self):
        self.sessions = {}
        self.available_tools = []

    async def connect_server(self, name: str, command: str, args: list):
        """Connect to an MCP server and discover its tools."""
        server_params = StdioServerParameters(command=command, args=args)
        transport = stdio_client(server_params)
        read, write = await asyncio.enter_async_context(transport)
        session = ClientSession(read, write)
        await session.initialize()

        # Discover available tools
        tools = await session.list_tools()
        self.sessions[name] = session
        self.available_tools.extend([
            {"name": tool.name, "description": tool.description, "server": name}
            for tool in tools.tools
        ])

    async def handle_request(self, user_message: str) -> str:
        """Process a user request using available tools."""
        # Build context with available tools
        tool_descriptions = "\n".join(
            f"- {t['name']} ({t['server']}): {t['description']}"
            for t in self.available_tools
        )

        prompt = f"""You are a customer support agent. Available tools:

{tool_descriptions}

User message: {user_message}

Decide which tools to call (if any) and respond to the user.
If you need to call a tool, respond with JSON: {{"tool": "tool_name", "args": {{...}}}}
If no tool is needed, respond directly."""

        # In production, this would call an LLM
        response = await self._call_llm(prompt)

        # Check if the LLM wants to call a tool
        if '"tool"' in response:
            tool_call = self._parse_tool_call(response)
            server_name = self._find_server(tool_call["name"])
            session = self.sessions[server_name]
            result = await session.call_tool(tool_call["name"], tool_call["args"])
            return f"Based on the system: {result}"

        return response

Step 4: Implement Escalation Logic

Not every query should be handled by AI. Implement confidence-based escalation:

from enum import Enum

class ConfidenceLevel(Enum):
    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"

def assess_confidence(agent_response: str, tools_used: list, user_sentiment: str) -> ConfidenceLevel:
    """Determine if the response should be escalated to a human."""
    # Low confidence signals
    if len(tools_used) == 0 and len(agent_response.split()) < 20:
        return ConfidenceLevel.LOW  # Very short answer without tool use

    if "I'm not sure" in agent_response or "I don't know" in agent_response:
        return ConfidenceLevel.LOW

    if user_sentiment == "angry":
        return ConfidenceLevel.LOW

    # High confidence signals
    if tools_used and "refund" not in str(tools_used):
        return ConfidenceLevel.HIGH

    if len(agent_response.split()) > 50 and tools_used:
        return ConfidenceLevel.HIGH

    return ConfidenceLevel.MEDIUM

async def handle_with_escalation(agent: SupportAgent, user_message: str):
    """Handle a request with automatic escalation."""
    response = await agent.handle_request(user_message)
    confidence = assess_confidence(response, agent.last_tools_used, agent.last_sentiment)

    if confidence == ConfidenceLevel.LOW:
        # Create a ticket and escalate
        ticket = await create_support_ticket(
            message=user_message,
            ai_response=response,
            reason="low_confidence",
        )
        return (
            f"I want to make sure you get the best help. I've connected you "
            f"with a human agent (Ticket #{ticket['id']}). They'll have full "
            f"context of our conversation."
        )

    return response

Step 5: Deploy With Streamable HTTP

For production, expose your MCP servers over HTTP instead of stdio:

from mcp.server import MCPServer
from mcp.server.auth import BearerTokenAuth

mcp = MCPServer(
    "order-service",
    auth=BearerTokenAuth(token="your-secret-token"),
)

# Same tool definitions as before...

if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)

The agent then connects via HTTP:

from mcp.client.auth import BearerTokenAuth

server_params = StreamableHTTPServerParameters(
    url="http://your-server:8000/mcp",
    auth=BearerTokenAuth(token="your-secret-token"),
)

Best Practices

  • Start with narrow scope: Build the refund flow first, then expand. A support agent that does one thing well is better than one that does five things poorly.
  • Log every tool call: Track which tools the agent uses, what arguments it passes, and whether the result was helpful. This data is invaluable for debugging and improvement.
  • Implement guardrails: Never let the agent execute destructive operations (refunds over $500, account deletions) without human confirmation.
  • Use structured responses: When calling tools, ensure responses are structured so the agent can parse and relay information accurately.
  • Build feedback loops: After each interaction, ask users if their issue was resolved. Use this data to fine-tune tool selection and response quality.

Common Pitfalls

  • Over-relying on the LLM for API calls: If the agent struggles with a complex API, build a simpler wrapper rather than letting the LLM guess at parameters.
  • Ignoring rate limits: Support systems handle high volume. Implement circuit breakers and rate limiting on all external API calls.
  • Skipping the knowledge base: Many support questions are simple FAQ lookups. A fast RAG retrieval for common questions saves API calls and reduces latency.
  • No fallback path: Every agent response path should have a human fallback. Users stuck in an AI loop will churn.

Conclusion and Next Steps

An agentic support system combines MCP servers for tool access, an LLM orchestrator for reasoning, and escalation logic for safety. Start by wrapping your most-used support tools as MCP servers, then layer in the agent orchestration.

The natural progression: add ticketing integration, then analytics to track resolution rates, and finally A/B testing to optimize tool selection prompts. The MCP standard means your servers work across any client — Claude, ChatGPT, or custom agents.