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?"
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, pino with EXACT versions from references/examples.md
- Python: Use
mcp>=1.0.0, starlette>=0.40.0, uvicorn>=0.30.0 with EXACT versions from references/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 confirmorcancel[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 meta with no content array. Your handlecalltool must handle empty content gracefully — do not default to a fallback text when meta is present. See server.py in references/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 Server class (not McpServer), meta is not passed as a function parameter to @server.calltool() handlers. You must read it from server.requestcontext.meta.modeldump(). Passing meta as a handler parameter will silently be None, causing threadid lookups to fail. See server.py in references/examples.md for 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
resources capability and handle empty resources list
- Use
registerToolsWithCustomerId wrapper to inject customerId from global store
See references/examples.md Section 2 (Server Core Infrastructure) for the complete implementation.
Welcome Tool
Requirements:
_meta.welcomeTool: true designates 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
transfertolive_agent extension
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_PASSWORD env 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 or String(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 description field is required by the DCA client's Pydantic validation — omitting it causes a server-side crash. Options can also carry per-option on_event callbacks 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): threadid (always present), locale (BCP 47, always present), wxoemailid, wxousername, wxotenant_id
- Channel Context (
com.ibm.orchestrate/channelcontext): channeltype + channel-specific attributes. Supported: slack, sip, genesysbotconnector, textmessaging, whatsapp, teams, chat, genesysaudioconnector
- Application 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:
transfertoliveagent: { messagetohumanagent, agentavailable, agentunavailable }
transfertochannel: { messagetouser, transfer_info: { target: { chat: { url } } } }
end_interaction: {} -- ends the conversation
speechtotext / texttospeech: { 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.md reference
Step 5: Implement Each Tool
- Review the relevant pattern specification above
- Present specification to user
- Confirm transaction pattern and widgets
- Implement following
references/examples.md reference
- 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.