Building MCP tools your CX agents actually use
Here are two descriptions of the same tool:
Version A: get_customer_info — Returns customer information.
Version B: get_customer_profile — Returns the customer's tier, membership date, lifetime spend, and their last 5 order summaries. Call this once at the start of any conversation where you don't already have account context. Do NOT call this on every turn.
Both are valid MCP tool definitions. The difference in agent behavior between them is enormous. With Version A, the agent calls the tool randomly, calls it multiple times, or skips it because "customer information" doesn't tell it what the tool returns or when to use it. With Version B, the agent calls it exactly once, at the right time, with accurate expectations about what it'll receive.
This article is about building the Version B version of every tool in your CX agent's toolset.
Tool descriptions are prompts: write them that way
Tool descriptions are the primary interface between your code and your agent's decision-making. The model reads them in the same forward pass where it decides what to do next, which means every tool description is effectively a prompt fragment that shapes behavior at every turn. Write them like system prompt instructions, not function signatures, and your agent will call the right tool at the right time.
The most impactful addition to any tool description is the "do NOT call this when" clause. Without it, agents over-call preemptively. They can't distinguish "this might be useful eventually" from "this is what I need right now." This applies to every tool in your toolset.
// Too vague: agent doesn't know when to call this or what it returns
const weakTool = {
name: "lookup_order",
description: "Look up order information.",
};
// Too long without structure: the key constraints get buried
const clutterTool = {
name: "lookup_order",
description: `
This tool connects to our order management system and retrieves
comprehensive order data including the order status, shipping details,
billing information, line items, fulfillment history, and any associated
customer service notes. It can be used in situations where the customer
is asking about their order or when you need detailed order information
to help resolve a support issue.
`,
};
// Right: clear return value, clear trigger, clear constraint
const goodTool = {
name: "get_order_details",
description: `
Returns full details for a specific order: status, line items,
shipping tracking, and any previous agent notes.
Call when the customer references a specific order ID or needs exact
shipping or billing details to resolve their issue.
Do NOT call preemptively — use the account summary first and only
look up a specific order when the customer references it.
`.trim(),
};The structure matters. Lead with what the tool returns. Follow with the trigger condition. End with the constraint. Three to five sentences is usually right: shorter and the agent lacks enough signal, longer and the signal gets buried.
The three tool categories every CX agent needs
CX agent tools fall into three natural categories. Organizing your toolset this way makes it easier for the agent to decide what to call at each step of a conversation, and easier for you to maintain the toolset as it grows.
Lookup tools are read-only data retrieval: customer profile, order details, ticket history, knowledge base search. These should be fast (under 300ms) and return structured data the agent can parse and cite directly in its response.
Action tools are write operations with side effects: create a ticket, update order status, process a refund, send an email confirmation. These need careful design because a mistake is hard to reverse. The description should include what the action does, what confirmation it returns, and any conditions under which it will fail or require human approval.
Escalation tools route the conversation to a human or a different system: schedule a callback, transfer to a specialist, create an urgent ticket. These should always be available, but the descriptions need to make clear when they're appropriate so the agent doesn't escalate prematurely.
Keep the count reasonable within each category. Three to four lookup tools, two to three action tools, and one to two escalation tools is a solid starting point for a CX agent. This gives enough capability to handle complex scenarios without overwhelming the tool selection step.
Building a CRM connector
Your CRM is usually the first integration a CX agent needs. Here's how to build it as an MCP server using the @modelcontextprotocol/sdk TypeScript package.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{ name: "crm-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "get_customer_profile",
description: `
Returns the customer's account tier, membership date, lifetime spend,
and summaries of their last 5 orders.
Call once at the start of a conversation when you don't already have
account context. Do NOT call this on every turn — only call again if
the customer switches topics or references a different account.
`.trim(),
inputSchema: {
type: "object",
properties: {
customer_id: {
type: "string",
description: "The customer ID from the conversation context."
}
},
required: ["customer_id"]
}
},
{
name: "get_order_details",
description: `
Returns full details for a specific order: status, line items,
shipping tracking number, and any previous agent notes.
Call when the customer references a specific order or when you need
exact details to resolve their issue. Do NOT call preemptively.
`.trim(),
inputSchema: {
type: "object",
properties: {
order_id: {
type: "string",
description: "The order ID referenced by the customer."
}
},
required: ["order_id"]
}
},
{
name: "create_support_ticket",
description: `
Creates a support ticket and returns the ticket ID and estimated
response time. The customer receives an email confirmation automatically.
Call when the issue can't be resolved in this conversation, or when
the customer explicitly asks for a ticket. Do NOT create a ticket
for issues you've already resolved in this conversation.
`.trim(),
inputSchema: {
type: "object",
properties: {
customer_id: { type: "string" },
issue_summary: {
type: "string",
description: "1-3 sentence description of the unresolved issue."
},
priority: {
type: "string",
enum: ["low", "normal", "high", "urgent"],
description: "Use 'urgent' only for billing failures or data loss."
}
},
required: ["customer_id", "issue_summary", "priority"]
}
}
]
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "get_customer_profile":
return {
content: [{
type: "text",
text: JSON.stringify(await crm.getCustomerProfile(args.customer_id))
}]
};
case "get_order_details":
return {
content: [{
type: "text",
text: JSON.stringify(await crm.getOrderDetails(args.order_id))
}]
};
case "create_support_ticket": {
const ticket = await crm.createTicket({
customerId: args.customer_id,
summary: args.issue_summary,
priority: args.priority,
});
return {
content: [{
type: "text",
text: JSON.stringify({
ticket_id: ticket.id,
estimated_response: ticket.estimatedResponseTime,
customer_message: `Ticket ${ticket.id} created. You'll receive an email confirmation shortly.`
})
}]
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
// Return structured errors the agent can reason about
return {
content: [{
type: "text",
text: JSON.stringify({
error: true,
code: (error as any).code ?? "TOOL_ERROR",
message: (error as any).message,
suggested_action: "escalate_to_human"
})
}]
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);The error handling pattern at the end is critical. When a tool call fails, the agent receives a structured error object it can reason about: what went wrong and what to do next. This prevents the agent from generating a confident-sounding response when the underlying tool silently failed.
The suggested_action field is a cue. The agent doesn't have to follow it, but it gives it a clear path forward without hallucinating a resolution it can't deliver.
Building a knowledge base retrieval tool
A useful knowledge retrieval tool returns 2 to 3 highly relevant results with relevance scores — not 10 loosely related ones. When the agent receives too many results, it tries to synthesize them all and produces hedged, generic responses instead of specific, citable answers. Limit the return set and structure results so the agent can evaluate relevance before deciding what to cite.
Here's how to build that tool and structure its returns:
// Tool definition
{
name: "search_knowledge_base",
description: `
Searches product documentation, policies, and FAQs.
Returns up to 3 relevant sections with relevance scores.
Call when you need policy information, technical specs, or procedures
you don't have in your current context.
For customer-specific data (orders, accounts, tickets), use the
CRM tools instead — don't search the knowledge base for account info.
`.trim(),
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "The specific question or topic to search for."
},
section: {
type: "string",
enum: ["returns", "shipping", "billing", "technical", "policies", "all"],
description: "Narrow to a specific section if the topic is clear. Defaults to 'all'."
}
},
required: ["query"]
}
}
// Handler
case "search_knowledge_base": {
const results = await knowledgeBase.search({
query: args.query,
section: args.section ?? "all",
topK: 3,
});
return {
content: [{
type: "text",
text: JSON.stringify({
query: args.query,
results: results.map(r => ({
title: r.title,
section: r.section,
relevance_score: r.score, // 0.0 to 1.0
content: r.content.slice(0, 600), // trim to avoid bloat
source_url: r.url,
})),
total_found: results.length
})
}]
};
}The relevance_score field is worth including even if your vector store doesn't return it natively. Normalize it or estimate it from distance metrics. Agents use relevance cues to decide how confidently to cite a result and whether to search again with a different query if the top result isn't strong.
The tool description also separates knowledge base queries from CRM queries explicitly. Without that boundary, agents will try to answer "what's the status of my order?" by searching the knowledge base, fail to find anything useful, and produce a confused response instead of calling the right tool.
Designing action tools with guardrails
Action tools need two built-in guardrails: a structured confirmation return that tells the agent exactly what happened, and an idempotency key that prevents the same action from firing twice in the same conversation. Without these, the most common failure modes are agents that understate what they did and agents that accidentally repeat an action when the customer asks a follow-up question.
Here's how to build both into every action tool:
Confirmation returns. Every action tool should return a confirmation object that describes what happened in plain language the agent can relay to the customer.
// Weak return: just success/failure
return { success: true };
// Strong return: confirmation the agent can use in its response
return {
success: true,
action_taken: "Refund of $47.99 initiated for order #ORD-8847",
timeline: "3-5 business days to your original payment method",
reference_number: "REF-20260624-1847",
customer_message: "Your refund of $47.99 has been processed. It will appear in 3-5 business days."
};The customer_message field gives the agent a ready-made confirmation phrasing. It doesn't have to use it verbatim, but it tells the agent what the confirmation should convey. This reduces the chance of the agent understating ("the refund might be processed") or overstating ("you'll see the refund today") what actually happened.
Idempotency keys. Pass a conversation ID or turn ID as an idempotency key on every action tool so that if the agent calls the same action twice — which happens more than you'd expect — the second call is a no-op.
case "process_refund": {
const refund = await payments.processRefund({
orderId: args.order_id,
amount: args.amount,
reason: args.reason,
idempotencyKey: `${args.conversation_id}-${args.order_id}-refund`,
});
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
refund_id: refund.id,
amount: refund.amount,
customer_message: `Refund of $${refund.amount} processed. Expect it in 3-5 business days.`
})
}]
};
}The conversation flow that makes this pattern visible:
The agent never acts before it knows the facts. Lookup tools fire first, action tools fire only when the agent has verified eligibility and the customer has confirmed intent.
Monitoring tool call quality in production
Tool call quality in production comes down to four metrics: call rate per tool (is it being called at the right frequency?), error rate per tool (is the integration reliable?), latency p95 (is this tool slowing down your conversational turns?), and escalation rate following tool failure (is a broken tool causing customers to give up?). Track all four from day one.
The most actionable starting point is error rate. Anything above 3% needs investigation: it's usually a data quality issue (malformed inputs from the agent) or a brittle external integration.
Chanl's MCP monitoring shows tool call frequency, latency, and error rate across your agent fleet. The conversation monitoring view lets you trace individual conversations and see exactly which tools were called at each turn and what they returned.
import { Chanl } from '@chanl/sdk';
const chanl = new Chanl({ apiKey: process.env.CHANL_API_KEY });
async function instrumentedToolCall(
toolName: string,
params: Record<string, unknown>,
conversationId: string
): Promise<ToolResult> {
const start = performance.now();
try {
const result = await callTool(toolName, params);
await chanl.tools.list({
toolName,
conversationId,
latencyMs: performance.now() - start,
success: true,
resultTokens: countTokens(JSON.stringify(result)),
});
return result;
} catch (error) {
await chanl.tools.list({
toolName,
conversationId,
latencyMs: performance.now() - start,
success: false,
errorCode: (error as any).code,
});
return {
error: true,
code: (error as any).code,
message: (error as any).message,
suggested_action: "escalate_to_human",
};
}
}The four metrics to watch in production:
Call rate per tool. How often is each tool being called per conversation? A tool with a near-zero call rate might have a description that's too restrictive. A tool with a very high call rate might be getting called preemptively when it shouldn't be.
Error rate per tool. Above 3% signals a brittle integration or a data quality issue. Track this separately from "the agent called the tool with bad parameters" versus "the tool itself failed."
Latency p95. Your agent's response time is the sum of inference time plus tool latency. Slow tools are usually the culprit for conversations that feel laggy. Set a per-tool latency SLO and alert when it's breached.
Escalation rate following tool failure. If customers escalate to humans after a specific tool fails, that tool is business-critical and needs a fallback path. Build it before it becomes a customer experience problem.
You can also use Chanl's testing scenarios to run your agent against synthetic conversations that exercise specific tool call paths. This is the fastest way to catch tool description regressions before they hit live traffic.
Tool design is a long-term investment
The tools you build today will be the foundation of every agent improvement you make over the next year. A well-designed CRM connector or knowledge retrieval tool will serve you through multiple prompt changes, model upgrades, and agent architecture shifts. A poorly designed one will cause problems at every step.
If you've been following the discussion about tool overload in CX agents, the resolution isn't fewer tools — it's better-designed tools with clear descriptions and well-scoped purposes. The 8-to-15 tool range hits a sweet spot where the agent has enough reach to handle complex scenarios without losing the signal in the tool selection step.
The tool calling fragmentation article covers why MCP matters as the standard here: without a consistent protocol, every tool integration is a bespoke integration. With MCP, your CRM server can be upgraded or swapped without touching agent code. The tool descriptions are the interface — get those right and the rest follows.
Start with the three categories: lookup, action, and escalation. Get those right before expanding. The quality of your first set of tools will set the standard for everything that comes after. Well-designed tools are the Connect layer in building, connecting, and monitoring AI agents for CX — they're what lets your agent reach into the real systems customers care about.
For a detailed look at how tools fit into the broader message flow between agents and orchestration layers, see the agent protocol stack guide.
Connect your CRM and knowledge base in minutes
Chanl's MCP runtime connects to any compliant tool server. Bring your own CRM, knowledge base, and ticket system, then monitor every tool call across your full agent fleet.
Start freeCo-founder
Building the platform for AI agents at Chanl — tools, testing, and observability for customer experience.
Learn Agentic AI
Weekly. Patterns for shipping agents that work. MCP, scorecards, regression tests, prompts, model comparisons.



