| name | customercare-mcp-builder |
| description | Build MCP servers for customer care agents following Watson Orchestrate specifications. Guides agents through tool creation, transaction patterns, authentication, widgets, and context management with strict adherence to reference specifications. Use when creating customer care MCP servers. |
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 confirm_or_cancel_[action]
Confirm/cancel tool config:
config: {
_meta: {
ui: { visibility: ['app'] }
}
}
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 handle_call_tool 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',
arguments: { 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:
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;
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.call_tool() handlers. You must read it from server.request_context.meta.model_dump(). Passing _meta as a handler parameter will silently be None, causing thread_id 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
transfer_to_live_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,
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,
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,
is_multi_selection?: boolean,
options: Array<{ label: string, value: string, description?: string,
on_event?: { tool: string, parameters: object } }>,
on_event: Array<{ event_type: 'message' | 'tool', option_value: string, tool?: string, parameters?: object, map_input_to?: string }>,
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): thread_id (always present), locale (BCP 47, always present), wxo_email_id, wxo_user_name, wxo_tenant_id
-
Channel Context (com.ibm.orchestrate/channelcontext): channel_type + channel-specific attributes. Supported: slack, sip, genesys_bot_connector, text_messaging, whatsapp, teams, chat, genesys_audio_connector
-
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:
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 conversation
speech_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.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.