Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Guide for creating production-grade ChatKit chatbots that integrate OpenAI Agents SDK with MCP tools and custom backends. Use when building AI-powered chatbots with specialized capabilities, real-time task execution, and user isolation for any application.
.claude/skills/aiskillstore-chatkit-botbuilder/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-20 | ✗→✓ | ▲ Improved | 151% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 70% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 114% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 120% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 78% | 0% |
Create production-grade chatbots using the OpenAI ChatKit framework. This skill enables building chatbots that:
This skill provides the complete architecture pattern for ChatKit integration, from frontend configuration to backend server implementation.
Use this skill when you need to:
User Message
↓
ChatKit Frontend (React/Next.js)
↓ [JWT Token in Authorization Header]
↓
FastAPI Backend (ChatKit Server)
↓ [Extract user_id from JWT]
↓
OpenAI Agent (Agents SDK)
↓ [Needs tool execution]
↓
MCP Tools (Custom Tool Functions)
↓ [Creates/Updates/Lists data]
↓
Database (User-Isolated Data)
↓
Response → ChatKit → Frontend → User1. Create ChatKit Server Class
pythonfrom chatkit.server import ChatKitServer from chatkit.store import Store class MyChatKitServer(ChatKitServer): def __init__(self): store = CustomChatKitStore() super().__init__(store=store) async def respond(self, thread, input, context): """Process user message and stream AI response""" user_id = getattr(context, 'user_id', None) # Create agent with wrapped tools # Stream response using official pattern
2. Create MCP Tool Wrappers
python# Extract user_id from context and inject into tool calls def add_task_wrapper(title: str, description: str = None): return mcp_add_task(user_id=user_id, title=title, description=description) def list_tasks_wrapper(status: str = "all"): return mcp_list_tasks(user_id=user_id, status=status)
3. Create FastAPI Endpoint
python@router.post("/api/v1/chatkit") async def chatkit_protocol_endpoint(request: Request): user_id = request.state.user_id # From JWT middleware context = create_context_object(user_id=user_id) result = await chatkit_server.process(body, context) return StreamingResponse(result, media_type="text/event-stream")
4. Configure JWT Middleware
pythonclass JWTAuthMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): # Extract JWT token from Authorization header # Decode and set request.state.user_id # All endpoints have access to authenticated user_id
1. Configure ChatKit SDK
typescriptconst chatKitConfig: UseChatKitOptions = { api: { url: `${API_BASE_URL}/api/v1/chatkit`, domainKey: 'your-domain-key', fetch: authenticatedFetch, // Custom fetch with JWT }, theme: 'light', header: { enabled: true, title: { text: 'AI Chat' } }, history: { enabled: true }, }
2. Create Authenticated Fetch Wrapper
typescriptasync function authenticatedFetch(input, options) { const token = localStorage.getItem('access_token') const headers = { ...options?.headers, 'Authorization': `Bearer ${token}`, } return fetch(input, { ...options, headers }) }
3. Integrate ChatKit Widget
typescriptimport { ChatKitWidget } from '@openai/chatkit-react' export default function Dashboard() { return ( <div className="flex gap-4"> {/* Your app content */} {showChat && ( <ChatKitWidget {...chatKitConfig} /> )} </div> ) }
4. Add Auto-Refresh for Real-Time Sync
typescriptuseEffect(() => { if (!showChatKit) return // Refresh immediately when chat opens fetchTasks() // Refresh every 1 second for real-time updates const interval = setInterval(() => { fetchTasks() }, 1000) return () => clearInterval(interval) }, [showChatKit])
1. Create MCP Tools with User Isolation
pythondef add_task(user_id: str, title: str, description: Optional[str] = None): """Create task - receives user_id from wrapper""" task = Task( id=str(uuid.uuid4()), user_id=user_id, # Critical: ensure user isolation title=title, description=description, completed=False, created_at=datetime.utcnow(), ) with Session(engine) as session: session.add(task) session.commit()
2. Register MCP Tools
pythonmcp_server = MCPServer() mcp_server.register_tool("add_task", add_task) mcp_server.register_tool("list_tasks", list_tasks) mcp_server.register_tool("delete_task", delete_task) # ... more tools
Three-Level Guarantee:
python# Middleware extracts user_id from token request.state.user_id = payload.get("user_id") # Tool wrapper captures and injects it def add_task_wrapper(title): return mcp_add_task(user_id=user_id, ...) # Database enforces it WHERE user_id = ? AND task_id = ?
User sends: "Create a task called 'Buy milk'"
↓
ChatKit Protocol: POST /api/v1/chatkit
Header: Authorization: Bearer <JWT>
Body: { "type": "message", "text": "Create..." }
↓
JWT Middleware:
Extracts user_id from token → request.state.user_id
↓
ChatKit Server (MyChatKitServer.respond):
Gets user_id from context
Creates wrapper functions capturing user_id
Passes wrappers to Agent
↓
OpenAI Agent:
Receives message: "Create a task..."
Selects tool: add_task_wrapper
Calls: add_task_wrapper(title="Buy milk")
↓
Wrapper Function:
Calls: mcp_add_task(user_id="user-123", title="Buy milk")
↓
MCP Tool:
Creates task with correct user_id
Returns: {"task_id": "...", "title": "Buy milk"}
↓
Agent Response:
"I've created 'Buy milk' task ✓"
↓
ChatKit Frontend:
Displays response
Auto-refreshes task list → Sees new taskpython# Official ChatKit pattern using Runner.run_streamed result = Runner.run_streamed( task_agent.agent, agent_input, context=agent_context, ) # Stream events using official stream_agent_response async for event in stream_agent_response(agent_context, result): yield event
python# Add user message to thread await self.store.add_thread_item(thread.id, input, context) # Load conversation history items_page = await self.store.load_thread_items( thread.id, after=None, limit=30, order="desc", context=context, ) # Convert to agent input agent_input = await simple_to_agent_input(items)
What it does:
Files to reference:
What it does:
Key setup:
What it does:
Implementation:
Root Cause: user_id not passed to MCP tools
Solution: Use wrapper functions that capture and inject user_id
pythondef add_task_wrapper(title): return mcp_add_task(user_id=user_id, title=title)
Root Cause: Missing user_id filter in database queries
Solution: Always filter by user_id at the tool level
pythonstmt = select(Task).where( Task.user_id == user_id, Task.completed == False )
Root Cause: Router not included in FastAPI app
Solution: Include router in main.py
pythonfrom routes import chatkit app.include_router(chatkit.router)
Root Cause: Custom fetch not adding JWT token
Solution: Ensure authenticatedFetch adds Bearer token
typescriptconst token = localStorage.getItem('access_token') headers['Authorization'] = `Bearer ${token}`
For true real-time (not polling):
Structure tool responses for ChatKit widgets:
pythonreturn { "tasks": task_list, "total": len(task_list), "pending": pending_count, "message": "You have 5 tasks", "widget": { "type": "card", "items": formatted_items, } }
Store conversation history in database:
This skill includes comprehensive resources for building ChatKit chatbots:
Backend Architecture: Complete FastAPI ChatKit server implementation details and patterns
Frontend Integration: Next.js ChatKit widget configuration and authentication
MCP Tools Guide: Creating wrapped tool functions with automatic user_id injection
User Isolation: Three-level user isolation strategy and verification checklist
chatkit_server_template.py - FastAPI ChatKit server boilerplate with all required methods
mcp_wrapper_generator.py - Script to auto-generate MCP tool wrappers
frontend_config_generator.ts - TypeScript config generator for ChatKit frontend setup
chatkit-nextjs-template/ - Complete Next.js project with ChatKit integrated
fastapi-backend-template/ - Complete FastAPI backend with ChatKit server implementation
When building a ChatKit chatbot, verify:
assets/fastapi-backend-template/ and assets/chatkit-nextjs-template/| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-20 | fail→pass | 15,650 | 16,186 | +3% | 1 | 1 | 0% | 2,632 | 6,617 | +151% | 0 | 0 | — |
case-01 | fail→pass | 21,054 | 19,147 | -9% | 1 | 1 | 0% | 4,280 | 7,294 | +70% | 0 | 0 | — |
case-02 | fail→pass | 17,847 | 19,854 | +11% | 1 | 1 | 0% | 3,615 | 7,743 | +114% | 0 | 0 | — |
case-03 | pass→pass | 18,999 | 19,727 | +4% | 1 | 1 | 0% | 3,660 | 7,469 | +104% | 0 | 0 | — |
case-04 | fail→pass | 14,952 | 9,343 | -38% | 1 | 1 | 0% | 2,394 | 5,255 | +120% | 0 | 0 | — |
case-05 | fail→pass | 14,052 | 4,640 | -67% | 1 | 1 | 0% | 2,431 | 4,324 | +78% | 0 | 0 | — |
case-06 | pass→pass | 13,888 | 16,847 | +21% | 1 | 1 | 0% | 2,334 | 5,459 | +134% | 0 | 0 | — |
case-07 | pass→pass | 15,803 | 10,104 | -36% | 1 | 1 | 0% | 2,631 | 5,321 | +102% | 0 | 0 | — |
case-08 | fail→pass | 11,511 | 6,715 | -42% | 1 | 1 | 0% | 1,704 | 4,648 | +173% | 0 | 0 | — |
case-09 | fail→pass | 9,876 | 3,898 | -61% | 1 | 1 | 0% | 1,694 | 4,225 | +149% | 0 | 0 | — |
case-10 | fail→pass | 10,628 | 7,300 | -31% | 1 | 1 | 0% | 1,808 | 4,868 | +169% | 0 | 0 | — |
case-11 | fail→pass | 10,965 | 7,395 | -33% | 1 | 1 | 0% | 1,748 | 4,816 | +176% | 0 | 0 | — |
case-12 | pass→pass | 14,074 | 12,469 | -11% | 1 | 1 | 0% | 2,255 | 5,569 | +147% | 0 | 0 | — |
case-13 | fail→pass | 11,526 | 6,228 | -46% | 1 | 1 | 0% | 1,865 | 4,513 | +142% | 0 | 0 | — |
case-14 | pass→pass | 15,733 | 12,394 | -21% | 1 | 1 | 0% | 2,735 | 5,933 | +117% | 0 | 0 | — |
case-15 | pass→pass | 8,712 | 5,297 | -39% | 1 | 1 | 0% | 1,398 | 4,360 | +212% | 0 | 0 | — |
case-16 | pass→pass | 13,669 | 9,769 | -29% | 1 | 1 | 0% | 2,358 | 5,349 | +127% | 0 | 0 | — |
case-17 | pass→pass | 9,849 | 6,176 | -37% | 1 | 1 | 0% | 1,794 | 4,582 | +155% | 0 | 0 | — |
case-18 | fail→pass | 11,367 | 3,388 | -70% | 1 | 1 | 0% | 1,930 | 4,104 | +113% | 0 | 0 | — |
case-19 | pass→pass | 14,541 | 15,062 | +4% | 1 | 1 | 0% | 2,573 | 5,858 | +128% | 0 | 0 | — |
case-21 | pass→pass | 14,094 | 12,820 | -9% | 1 | 1 | 0% | 2,562 | 6,040 | +136% | 0 | 0 | — |
case-22 | pass→fail | 20,031 | 21,042 | +5% | 1 | 1 | 0% | 3,408 | 7,308 | +114% | 0 | 0 | — |
case-23 | pass→pass | 6,339 | 5,591 | -12% | 1 | 1 | 0% | 1,172 | 4,443 | +279% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +43 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.