a
Scenario 01 · Teaching Aid

Customer Support Resolution Agent

Learn how to design and build an AI agent that resolves support tickets with 80%+ first-contact resolution, using MCP tools and smart escalation logic.

Claude Agent SDK MCP Tools Escalation Logic High Ambiguity Returns · Billing · Account

What makes this agent different?

01 🎯

High-Ambiguity Handling

Customers rarely describe problems clearly. The agent must infer intent from vague phrases like "my thing doesn't work" or "I want a refund" — mapping them to concrete backend operations.
02 🔧

MCP Tool Use

Instead of hallucinating answers, the agent calls verified backend tools: get_customer, lookup_order, process_refund, escalate_to_human.
03

First-Contact Resolution

The 80% FCR target means most issues should be solved in ONE interaction, not handed off. This changes how you design the system prompt and tool permissions.
04 🧠

Contextual Memory

The agent maintains conversation state — it knows what it looked up, what it offered, and what the customer said. Each tool call enriches the context window.
05 🚨

Smart Escalation

Escalation isn't failure — it's the right call when: emotion is high, the issue is policy-level, legal risk exists, or the agent has tried and failed.
06 🔒

Guard Rails & Safety

The agent needs hard limits: never process refunds above $X without confirmation, never delete account data, always verify identity before sharing PII.

Click a node to learn more

step 01 Intake
Parse intent
step 02 Verify Identity
get_customer()
step 03 Context Lookup
lookup_order()
step 04 Classify Issue
Routing logic
✓ Resolve → process_refund()
? Clarify → ask follow-up
↑ Escalate → escalate_to_human()

👆 Click any node above to see what happens at that step.

Click each tool to explore its interface

get_customer() READ

Fetches a customer record from your CRM by email, phone, or ID. Always call this first.

identifierstring — email, phone, or customer_id
returnsname, account_status, tier, lifetime_value, open_tickets
⚠ TipIf not found, don't assume fraud — ask for alternate ID.
# Example MCP call get_customer({ "identifier": "[email protected]" }) # Returns: { "customer_id": "cust_882", "name": "Alice Chen", "tier": "gold", "account_status": "active", "lifetime_value": 2840 }
lookup_order() READ

Retrieves full order details including status, items, fulfilment, and return eligibility.

order_idstring — order number
returnsitems, status, delivery_date, return_window, refund_amount
💡 PatternIf customer doesn't know order ID, use customer_id to get recent orders first.
lookup_order({ "order_id": "ORD-4421" }) # Returns: { "status": "delivered", "delivered_at": "2025-02-10", "return_eligible": true, "return_window_days": 5, "refund_amount": 89.99 }
process_refund() WRITE

Initiates a refund against a verified order. Must confirm eligibility and get customer consent first.

order_idstring — must match verified customer
reasonenum — DEFECTIVE | NOT_RECEIVED | CHANGED_MIND | OTHER
amountfloat — defaults to full order amount
🔒 Guard railIf amount > $200, require agent to confirm with customer before calling.
process_refund({ "order_id": "ORD-4421", "reason": "NOT_RECEIVED", "amount": 89.99 }) # Returns: { "refund_id": "REF-991", "status": "approved", "eta_days": 3 }
escalate_to_human() ESCALATE

Hands off the conversation to a human agent with full context. Last resort, not first instinct.

customer_idstring — for queue routing
reasonstring — why escalation is needed
summarystring — what the agent learned so far
priorityenum — LOW | MEDIUM | HIGH | URGENT
💡 Best practiceAlways tell the customer you're escalating AND why. Never ghost-transfer.
escalate_to_human({ "customer_id": "cust_882", "reason": "Legal dispute, customer threatening chargeback", "summary": "Order ORD-4421, refund denied due to window expiry", "priority": "HIGH" })

Watch the agent reason through real requests

🔐 Locked Account

😶
Customer
I can't log into my account. It says it's locked. I need to access my invoices urgently.
🤖
Agent (thinking)
→ get_customer() → 2 previous tickets, unresolved. Amount: $400 (> $200 limit). Chargeback mentioned → ESCALATE
😶
Customer
Yes, that would be great, thank you!
🤖
Aria (Agent)
I'll help you get back in right away. Could you share the email address associated with your account?
🤖
Aria (Agent)
Given the amount and history, I'm escalating this to our senior team right now with full notes, so you won't have to explain again.
🤖
Agent (thinking)
→ Calling process_refund(reason="OTHER", amount=149.00) → approved
😶
Customer
🤖
Agent (thinking)
↑ escalate_to_human(priority="HIGH", summary="3rd contact, unresolved $400 refund, chargeback threat")
🤖
Aria (Agent)
$149 refund approved (Ref: REF-1102). It will appear in 3 - 5 days. Your account is now fully cancelled with no future charges.
✓ First-Contact Resolved
🤖
Agent (thinking)
→ get_customer("[email protected]") → account_status: LOCKED, reason: 5 failed login attempts, no fraud flag
🤖
Aria (Agent)
You're now in our priority queue. A senior agent will be with you in approximately 2 minutes. Your reference is ESC-441.
→ Escalated to Human → correct call
😶
Aria (Agent)
Your account was locked after 5 failed login attempts as a security measure. I can unlock it now and send a password reset link. Shall I send it to [email protected]?
😶
Customer
Yes please!
🤖
Agent (thinking)
→ Account unlocked, reset email triggered
🤖
Aria (Agent)
Done! Your account is unlocked and a password reset email is on its way. Your invoices are under Account → Billing History.
✓ First-Contact Resolved

What does success look like?

80%+
First-Contact Resolution
< 90s
Avg. Handle Time
4.3+
CSAT Score / 5
20%
Max Escalation Rate

Why these numbers are hard to hit — and how the agent helps

FCR is hard because customers give incomplete info, backend systems have gaps, and agents over-escalate out of caution. Claude solves this by using MCP tools to get real data before making any decision.

Handle time drops because the agent doesn't ask questions it can already answer with a tool call.

CSAT goes up when customers feel heard and resolved fast. The agent should acknowledge emotion before jumping to solutions.

Escalation rate must be monitored — too low means the agent is handling things it shouldn't, too high kills the ROI.

Reference implementation using the Claude Agent SDK

system_prompt.txt
You are a customer support resolution agent for Acme Corp. ## Identity Your name is Aria. You are empathetic, efficient, and empowered to resolve most issues without escalation. ## Capabilities You have access to 4 tools: - get_customer(identifier) → Look up customer record - lookup_order(order_id) → Get order details & eligibility - process_refund(order_id, ...) → Issue a refund - escalate_to_human(...) → Hand off to human agent ## Resolution Rules 1. Always call get_customer() first to verify identity 2. Call lookup_order() before discussing any order issue 3. You MAY process refunds if: - Order is within return window - Amount is ≤ $200 - Customer is not flagged for abuse 4. For amounts over $200, confirm with customer before processing 5. Acknowledge customer emotion BEFORE jumping to solutions ## MUST Escalate When: - Customer mentions legal action or chargebacks - Account is flagged for fraud investigation - Issue requires policy exception beyond your authority - Customer has expressed anger 3+ times in conversation - You cannot resolve after 2 clarifying attempts ## Tone Warm but efficient. Never defensive. Apologize for experience, not necessarily for company fault. Be specific about ETAs. ## Never - Share another customer's data - Make promises outside policy - Process refunds for fraud-flagged accounts - Reveal internal system details
agent.py
import anthropic from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client client = anthropic.Anthropic() async def run_support_agent(customer_message: str): server_params = StdioServerParameters( command="python", args=["-m", "support_mcp_server"] ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools() claude_tools = [ {"name": t.name, "description": t.description, "input_schema": t.inputSchema} for t in tools.tools ] messages = [{"role": "user", "content": customer_message}] while True: response = client.messages.create( model="claude-opus-4-5", max_tokens=4096, system=SYSTEM_PROMPT, tools=claude_tools, messages=messages ) if response.stop_reason == "end_turn": break if response.stop_reason == "tool_use": tool_results = [] for block in response.content: if block.type == "tool_use": result = await session.call_tool( block.name, block.input ) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": result.content[0].text }) messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": tool_results}) return extract_text(response.content)
mcp_tool_schema.json
{ "tools": [ { "name": "get_customer", "description": "Look up a customer by email, phone, or ID", "inputSchema": { "type": "object", "properties": { "identifier": { "type": "string", "description": "Email, phone number, or customer_id" } }, "required": ["identifier"] } }, { "name": "lookup_order", "description": "Get full details of an order including return eligibility", "inputSchema": { "type": "object", "properties": { "order_id": { "type": "string" } }, "required": ["order_id"] } }, { "name": "process_refund", "description": "Issue a refund for an eligible order", "inputSchema": { "type": "object", "properties": { "order_id": { "type": "string" }, "reason": { "type": "string", "enum": ["DEFECTIVE", "NOT_RECEIVED", "CHANGED_MIND", "OTHER"] }, "amount": { "type": "number" } }, "required": ["order_id", "reason"] } }, { "name": "escalate_to_human", "description": "Transfer conversation to a human support agent", "inputSchema": { "type": "object", "properties": { "customer_id": { "type": "string" }, "reason": { "type": "string" }, "summary": { "type": "string" }, "priority": { "type": "string", "enum": ["LOW", "MEDIUM", "HIGH", "URGENT"] } }, "required": ["customer_id", "reason", "summary"] } } ] }

Test your understanding

Q1 of 5: Which tool should almost always be the FIRST tool call in a support conversation?
→ get_customer() establishes identity → without it, you don't know who you're talking to.
Q2 of 5: A customer says "I want a refund" but their order was placed 45 days ago and the return window is 30 days. What should the agent do?
The agent should verify facts with lookup_order(), then empathetically explain and offer alternatives like store credit.
Q3 of 5: When should the agent NEVER process a refund without additional confirmation?
Large refunds are a common fraud vector. The system prompt should define a dollar threshold above which confirmation is required.
Q4 of 5: A customer mentions "I'm going to dispute this with my credit card company." What should the agent do?
Chargeback threats are a legal and financial trigger. This should be a mandatory escalation condition.
Q5 of 5: What MUST be included when calling escalate_to_human()?
The summary field prevents a terrible handoff experience. It should include who, what, what was tried, and why escalation is happening.