CustomerCare MCP Server Builder
This skill guides you through building production-ready MCP (Model Context Protocol) servers for customer care agents. Follow the specifications and patterns documented below exactly. All reference implementations are in references/examples.md.
CRITICAL INSTRUCTIONS FOR AGENTS
0. Ask About Programming Language
Before starting any implementation, ASK the user:
"Which programming language would you like to use for this MCP server?"
- TypeScript
- Python
Both languages have complete reference implementations in references/examples.md. Use the chosen language to build the MCP server.
1. ALWAYS Provide Tool Summary First
Before writing any code, create a comprehensive summary of all tools to be implemented:
- List each tool with its name, purpose, and pattern type
- Identify which tools need widgets, confirmations, or chaining
- Specify authentication requirements
- Document context management needs
Example Summary Format:
Tools to Create:
1. get_account_balance - Direct Response pattern, no confirmation
2. transfer_money - Transaction pattern (two-step), confirmation widget
3. update_profile - Hybrid Response, requires authentication check
4. welcome - Welcome tool (required on ALL servers)
5. knowledge_search - RAG integration (if OpenSearch available)
2. ALWAYS Confirm Specifications Before Coding
For EACH tool, before writing code:
- Review the relevant pattern from the Pattern Specifications section below
- Present the specification to the user and ask:
- "Should this tool use a transaction pattern with confirmation?"
- "What widgets are needed (picker, form, confirmation)?"
- "Should this be a direct response or hybrid response?"
- Reference implementation examples in
references/examples.md
DO NOT assume -- Always confirm with the user before implementing.
3. Welcome Tool is MANDATORY
Every MCP server MUST have a welcome tool. See the Welcome Tool pattern below.
Before creating the welcome tool, ASK the user:
- Authentication Method: JWT-based auto-auth or credential verification (PIN, password)?
- Welcome Message: What should it say? What tone?
- Target Audience: Who are the users (customers, employees, partners)?
- Schema Requirements: What user information needs to be collected?
Reference: See "Welcome & Authentication Tools" section in references/examples.md
4. Knowledge Tool -- Confirm OpenSearch Setup
Before implementing knowledge/RAG functionality, ASK:
- "Do you have an OpenSearch instance already set up?"
- "What indices/collections should be searched?"
If OpenSearch is NOT available, do NOT implement the knowledge tool.
Reference: See "Knowledge/RAG Tools" section in references/examples.md
5. Server Startup Code -- Follow Reference EXACTLY
Copy the server entry point from references/examples.md exactly. Key requirements:
- TypeScript: Use
@modelcontextprotocol/sdk,express,pinowith EXACT versions fromreferences/examples.md - Python: Use
mcp>=1.0.0,starlette>=0.40.0,uvicorn>=0.30.0with EXACT versions fromreferences/examples.md - MUST use
StreamableHTTPServerTransport(no session validation) - DO NOT use SSE transport SSE is not supported - Create per-request server instances based on customer authentication state
Folder Structure:
project/
├── src/
│ ├── index.ts (or main.py + server.py)
│ ├── globalStore.ts (or global_store.py)
│ ├── localStore.ts (or local_store.py)
│ ├── logger.ts (or logger.py)
│ ├── customerDatabase.ts (or customer_database.py)
│ ├── [tool_name].ts (or [tool_name].py)
│ └── [tool_name]Service.ts (or [tool_name]_service.py)
├── package.json (or pyproject.toml)
└── .env.example
6. Widget Definitions -- NO DEVIATIONS
Widget specifications MUST be followed exactly. See Widget Types Reference below.
7. Final Solution Must Be Error-Free and Executable
Before presenting final solution: validate all code against the pattern specifications below, verify widget definitions match the Widget Types Reference, ensure all imports are correct, and confirm startup scripts work.
Pattern Specifications
Transactions (Two-Step Confirmation)
When to use: Any sensitive or irreversible operation (money transfers, payments, cancellations, profile changes).
Pattern:
- Step 1 (prepare): Model-visible tool collects inputs via widgets, then shows confirmation widget. Stores transaction details in LOCAL store.
- Step 2 (confirm/cancel): Hidden tool (
visibility: ['app']) processes user's explicit confirm or cancel action.
Key Rules:
- Model can NEVER auto-confirm transactions
- Generate unique transaction ID per transaction
- Store details in LOCAL store (Layer 3), not global store
- Verify ownership before executing
- Clean up local variable after processing
Naming convention: prepare_[action] and confirm_or_cancel_[action]
Confirm/cancel tool config:
config: {
_meta: {
ui: { visibility: ['app'] } // Hidden from model -- only callable by UI widget
}
}
Confirmation widget schema:
{
type: 'confirmation',
title: 'Confirm Transfer',
confirmation_text: '## Confirm\n\n**Details...**',
on_confirm: {
tool: 'confirm_or_cancel_[action]',
parameters: { transactionId, action: 'confirm' },
},
on_cancel: {
tool: 'confirm_or_cancel_[action]',
parameters: { transactionId, action: 'cancel' },
},
}
⚠ Widget-only responses: Confirmation widgets (and other pure-widget steps like account pickers) often return only
_metawith nocontentarray. Yourhandle_call_toolmust handle empty content gracefully — do not default to a fallback text when_metais present. Seeserver.pyinreferences/examples.md.
Direct Response (Tool Responses to Users)
When to use: Financial amounts, regulatory-required info, dates, account numbers -- anything that must be exact with zero hallucination risk.
Pattern: Set annotations: { audience: ['user'] } on content blocks. Content goes directly to user, bypassing the LLM entirely.
return {
content: [{
type: 'text',
text: `Balance: $${balance.toFixed(2)}`,
annotations: { audience: ['user'] },
}],
};
Tradeoff: Model does NOT see this content, so it cannot reason about it. Use hybrid response when model needs context.
Hybrid Response
When to use: When you need both exact data display AND model awareness for conversation flow.
Pattern: Two content blocks -- one for model (audience: ['assistant']), one for user (audience: ['user']). structuredContent is always visible to the model.
return {
content: [
{ type: 'text', text: 'Payment history retrieved. Data displayed to user.', annotations: { audience: ['assistant'] } },
{ type: 'text', text: `**Payment History**\n\n${table}`, annotations: { audience: ['user'] } },
],
structuredContent: paymentData,
};
Tool Chaining
When to use: Deterministic tool-to-tool calls without LLM reasoning (e.g., locked account -> agent handoff).
Pattern: Return _meta.nextTool with tool name and arguments.
return {
content: [{ type: 'text', text: 'Account is locked.', annotations: { audience: ['user'] } }],
_meta: {
nextTool: {
tool: 'request_agent_handoff',
parameters: { reason: 'Locked account' },
},
},
};
Rules: Only for tool-to-tool chaining (not user input dependent). Client must implement depth limits (recommended: 5).
Three-Layer Context System
MCP tools have access to three layers of context:
| Layer | Namespace | Scope | Lifetime | Use For |
|---|---|---|---|---|
| 1. Context Variables | com.ibm.orchestrate/context |
Per-request | Single API call | Auth tokens (JWT), app-provided config |
| 2. Global Store | In-memory map | Per-thread | Until conversation ends | Data shared across MCP servers (customerId) |
| 3. Local Store | In-memory map | Per-thread per-server | Until thread ends | Pending transactions, workflow state |
Accessing context variables:
// TypeScript (McpServer high-level SDK) -- extra is passed automatically
const jwtToken = extra?._meta?.['com.ibm.orchestrate/context']?.jwtToken;
const threadId = extra?._meta?.['com.ibm.orchestrate/systemcontext']?.thread_id;
const locale = extra?._meta?.['com.ibm.orchestrate/systemcontext']?.locale;
# Python (low-level Server SDK) -- extract _meta from request context, NOT function parameters
ctx = server.request_context
extra = {}
if ctx and ctx.meta:
extra['_meta'] = ctx.meta.model_dump()
jwt_token = extra.get('_meta', {}).get('com.ibm.orchestrate/context', {}).get('jwtToken')
thread_id = extra.get('_meta', {}).get('com.ibm.orchestrate/systemcontext', {}).get('thread_id')
locale = extra.get('_meta', {}).get('com.ibm.orchestrate/systemcontext', {}).get('locale')
⚠ Python low-level SDK: When using the low-level
Serverclass (notMcpServer),_metais not passed as a function parameter to@server.call_tool()handlers. You must read it fromserver.request_context.meta.model_dump(). Passing_metaas a handler parameter will silently beNone, causing thread_id lookups to fail. Seeserver.pyinreferences/examples.mdfor the correct pattern.
Critical: NEVER trust the model to provide authentication data. Always inject customerId from the store or extract from JWT.
Unique Tools Per User
Pattern: Create per-request MCP server instances with only the tools relevant to a customer's products/entitlements.
- Unauthenticated users: only welcome + handoff tools
- After auth: register product-specific tools based on customer profile
- Must declare
resourcescapability and handle empty resources list - Use
registerToolsWithCustomerIdwrapper to inject customerId from global store
See references/examples.md Section 2 (Server Core Infrastructure) for the complete implementation.
Welcome Tool
Requirements:
_meta.welcomeTool: truedesignates automatic invocation at thread start- Must be
visibility: ['app'](hidden from model) - Empty
inputSchema(no required arguments) - Only ONE welcome tool invoked per thread, even across multiple servers
- Should execute quickly
After successful auth, signal tool refresh:
return {
content: [{ type: 'text', text: 'Verified!', annotations: { audience: ['user'] } }],
_meta: { refreshThreadCapabilities: threadId },
};
This triggers ALL MCP servers to refresh their tool/resources lists for the thread.
Authentication
Two approaches:
- Client Authentication (recommended): JWT passed via context variables. Tool extracts and uses directly.
- Agent Authentication (voice/IVR): PIN verification in-agent, stores customerId in global store, triggers
refreshThreadCapabilities.
After successful auth:
- Store customerId in global store:
setGlobalVariable(threadId, 'customerId', profile.customerId) - Return
_meta: { refreshThreadCapabilities: threadId } - Client refreshes tool lists from ALL MCP servers
- Servers check for customerId and return authenticated tools
Agent Handoff
Pattern: Two-step user-controlled handoff:
- Present options (realtime or callback) via options widget
- Process choice -- for realtime, use
transfer_to_live_agentextension
Extension schema:
_meta: {
'com.ibm.orchestrate/extensions': {
transfer_to_live_agent: {
message_to_human_agent: "Context for the agent...",
agent_available: "Please wait while I connect you.",
agent_unavailable: "No agents online. Try again later.",
}
}
}
Knowledge/RAG Integration
- Conditional registration: only when
OPENSEARCH_PASSWORDenv var is set - Returns both formatted text and
structuredContent - Configurable field mappings and query bodies via environment variables
- Uses neural/semantic search by default
Localization
- Model auto-localizes content that passes through it
- You MUST manually localize content with
audience: ['user']and all widget text - Access locale:
_meta['com.ibm.orchestrate/systemcontext'].locale(BCP 47 format) - Always fallback to
en-US - Use template-based localization with
{placeholder}syntax
Technical Reference
Model Visibility
Content visibility:
| Content Type | Model Sees? | User Sees? | Notes |
|---|---|---|---|
content (no audience annotation) |
Yes | Yes | Standard behavior for tool responses |
content with audience: ['user'] |
No | Yes | Goes directly to user, bypasses LLM |
content with audience: ['assistant'] |
Yes | No | Provides context to model without showing user |
structuredContent |
Yes | Yes | Always visible to model — use for data model should see and reason about |
_meta |
Never | Never | Hidden from model — use for widgets, internal data, extensions |
Tool visibility:
| Tool Configuration | Model Can See/Call? |
|---|---|
No _meta.ui.visibility |
Yes |
_meta.ui.visibility: ['app'] |
No -- only callable by system/widgets |
Widget Types Reference
Tools declare widget support via _meta.ui.resourceUri: "ui://ibm.com/orchestrate/widget". Widgets are returned in _meta['com.ibm.orchestrate/widget'].
ConfirmationWidget:
{
type: 'confirmation',
title: string,
confirmation_text?: string, // Supports markdown
on_confirm: { tool: string, parameters: { [key: string]: unknown } },
on_cancel: { tool: string, parameters: { [key: string]: unknown } },
}
DatetimeWidget:
{
type: 'datetime',
collection_type: 'date' | 'time' | 'datetime',
title: string,
description?: string,
min_datetime?: string, // YYYY-MM-DD for date, hh:mm for time
max_datetime?: string,
on_event: { tool: string, parameters: { [key: string]: unknown }, map_input_to: string },
}
NumberWidget:
{
type: 'number',
collection_type: 'integer' | 'currency' | 'percentage' | 'decimal' | 'phone' | 'zip_code',
title?: string,
description?: string,
min_number?: number,
max_number?: number,
min_digits?: number,
max_digits?: number,
on_event: { tool: string, parameters: { [key: string]: unknown }, map_input_to: string },
}
⚠ Type coercion: Number widgets send values as integers (e.g.,
1234). If comparing against a string field (like a PIN stored as"1234"), you must cast to string first:pin = str(args.get("pin", ""))in Python orString(pin)in TypeScript. Mismatched types will silently fail equality checks.
OptionsWidget:
{
type: 'options',
title: string,
description: string, // ⚠ Required by DCA client validation
is_multi_selection?: boolean,
options: Array<{ label: string, value: string, description?: string,
on_event?: { tool: string, parameters: object } }>, // Per-option events
on_event: Array<{ event_type: 'message' | 'tool', option_value: string, tool?: string, parameters?: object, map_input_to?: string }>,
// OR simplified form:
on_event: { tool: string, parameters: object, map_input_to: string },
}
⚠ Required fields: The
descriptionfield is required by the DCA client's Pydantic validation — omitting it causes a server-side crash. Options can also carry per-optionon_eventcallbacks for different actions per choice (e.g., "retry" vs "speak with agent").
TextWidget:
{
type: 'text',
collection_type: 'text' | 'regex',
title: string,
description?: string,
regex_expression?: string,
on_event: { tool: string, parameters: { [key: string]: unknown }, map_input_to: string },
}
Annotations Reference
Content block annotations modify rendering behavior via _meta['com.ibm.orchestrate/annotations']:
PauseAnnotation: { pause: { delay: 1000 } } -- millisecond delay before rendering
SpeechAnnotation:
{
speech: {
disable_speech_barge_in?: boolean,
disable_dtmf_barge_in?: boolean,
disable_speech_to_text?: boolean,
text_to_speech_config?: { voice?: string, [key: string]: unknown },
}
}
Media content via resource_link:
{ type: 'resource_link', resource: { uri: "https://example.com/audio.mp3", mimeType: "audio/mpeg", text: "Description" }, annotations: { audience: ["user"] } }
Context Variables Reference
Three namespaces:
System Context (
com.ibm.orchestrate/systemcontext):thread_id(always present),locale(BCP 47, always present),wxo_email_id,wxo_user_name,wxo_tenant_idChannel Context (
com.ibm.orchestrate/channelcontext):channel_type+ channel-specific attributes. Supported: slack, sip, genesys_bot_connector, text_messaging, whatsapp, teams, chat, genesys_audio_connectorApplication Context (
com.ibm.orchestrate/context): App-provided custom fields (e.g.,jwtToken,telephoneNumber). Flexible schema, always available.
Extensions Reference
Extensions in _meta['com.ibm.orchestrate/extensions'] signal client actions:
transfer_to_live_agent:{ message_to_human_agent, agent_available, agent_unavailable }transfer_to_channel:{ message_to_user, transfer_info: { target: { chat: { url } } } }end_interaction:{}-- ends the conversationspeech_to_text/text_to_speech:{ command_info: { type, parameters? } }
Specification Changes
refreshThreadCapabilities: When any MCP server returns _meta.refreshThreadCapabilities: threadId, the client refreshes tools/list and resources/list from ALL MCP servers for that thread. Critical for auth state changes.
Multiple Widgets Per Tool: Tools can declare _meta.ui.resourceUris: string[] (array) and dynamically select which widget via _meta.ui.resourceUri in the response. Response resourceUri MUST be in the declared array.
Tool Chaining Metadata: _meta.nextTool: { tool: string, arguments?: Record<string, any> }. Client must validate arguments, implement depth limits (5), and detect circular chains.
Welcome Tool Metadata: _meta.welcomeTool: true on tool registration. One per server max. One invoked per thread. Must work without required arguments.
Workflow for Building an MCP Server
Step 1: Choose Programming Language
- Ask the user: "Which programming language would you like to use: TypeScript or Python?"
- Use the chosen language consistently throughout the entire project
- Reference the appropriate examples from
references/examples.md
Step 2: Gather Requirements
- Ask about the domain and use case
- Identify required tools and their purposes
- Determine authentication requirements
- Check for OpenSearch/knowledge integration needs
Step 3: Create Tool Summary
- List all tools with their pattern types (direct response, hybrid, transaction, etc.)
- Identify widget requirements for each tool
- Document context management needs
- Present summary to user for confirmation
Step 4: Design Welcome Tool
- Ask authentication questions (see section 3 above)
- Confirm welcome message and tone
- Define schema and validation rules
- Implement based on
references/examples.mdreference
Step 5: Implement Each Tool
- Review the relevant pattern specification above
- Present specification to user
- Confirm transaction pattern and widgets
- Implement following
references/examples.mdreference - Validate against specifications
Step 6: Implement Server Core
- Copy server startup code from
references/examples.md - Use EXACT dependency versions from
references/examples.md - Maintain folder structure
- Configure logging (optional) and context management
Step 7: Add Knowledge Tool (if needed)
- Confirm OpenSearch availability
- Copy implementation from
references/examples.md - Update only configuration (endpoint, credentials, index names)
Step 8: Final Validation
- Review all code against pattern specifications
- Verify widget definitions match Widget Types Reference
- Ensure all imports are correct
- Confirm installation instructions work
Critical Reminders
- ALWAYS ask about programming language preference first (TypeScript or Python)
- ALWAYS provide tool summary first
- ALWAYS confirm specifications before coding
- ALWAYS ask about welcome tool requirements
- ALWAYS confirm OpenSearch setup for knowledge tools
- ALWAYS use exact server implementation from
references/examples.md - ALWAYS validate against pattern specifications
- NEVER deviate from widget schemas
- NEVER modify dependency versions
- NEVER assume -- always ask the user
License
MIT License - See individual server directories for complete license information.