@palbase/kibele-mcp
v0.1.4
Published
MCP server for Kibele — opinionated AI design critic. Analyze screenshots from Claude Code, Cursor, Claude Desktop, etc.
Readme
@palbase/kibele-mcp
MCP server for Kibele — Palaemon's opinionated AI design critic. Brings Kibele into Claude Code, Cursor, Claude Desktop, Cline, Zed — anywhere with MCP support.
Built on top of the Kibele backend's REST API. Sync + async tools, three input modes (file path, URL, drag-dropped base64), desktop notification when async jobs finish.
Why
The Figma plugin owns the designer's moment ("design ready for review"). This MCP brings Kibele into the developer's moment — review your SwiftUI / Compose / React Native screen right inside the IDE, with Hasammeli (the code scout) reading the actual source for code_location precision.
Install
cd kibele-mcp
npm install
npm run buildRun the Kibele backend
This MCP server is a thin client. The backend (/critique/analyze, /critique/flow) must be reachable.
# in the repo root:
npm run start:dev
# → Kibele listening on http://localhost:3000Wire it up
Claude Code
Add to ~/.config/claude-code/mcp.json (or your project's .mcp.json):
{
"mcpServers": {
"kibele": {
"command": "node",
"args": ["/absolute/path/to/kibele/kibele-mcp/dist/index.js"],
"env": {
"KIBELE_URL": "http://localhost:3000"
}
}
}
}Cursor
Add to .cursor/mcp.json in your project:
{
"mcpServers": {
"kibele": {
"command": "node",
"args": ["/absolute/path/to/kibele/kibele-mcp/dist/index.js"],
"env": { "KIBELE_URL": "http://localhost:3000" }
}
}
}Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"kibele": {
"command": "node",
"args": ["/absolute/path/to/kibele/kibele-mcp/dist/index.js"],
"env": { "KIBELE_URL": "http://localhost:3000" }
}
}
}API key (optional)
If the backend has KIBELE_API_KEY set, pass the same value to the MCP server:
"env": {
"KIBELE_URL": "http://localhost:3000",
"KIBELE_API_KEY": "your-key-here"
}Tools
| Tool | Mode | Latency | Use |
|---|---|---|---|
| kibele_analyze | sync | 100-260s | Single screen, blocks conversation |
| kibele_flow | sync | 200-400s | 2-12 screen flow, blocks conversation |
| kibele_start | async | <1s | Fire-and-forget. Returns runId. Notifies on done. |
| kibele_result | sync | <100ms | Poll a runId for status / result |
| kibele_list_runs | sync | <100ms | All runs in this MCP session |
| kibele_set_context | sync | <10ms | Required once per session. Sets app category + short description. |
| kibele_get_context | sync | <10ms | Read current session context |
| kibele_clear_context | sync | <10ms | Forget session context (use when switching projects) |
| kibele_set_profile | sync | <500ms | Create / update a persistent backend profile |
| kibele_health | sync | <500ms | Backend reachability check |
Session-scoped product context (required)
Before Kibele will critique anything, she needs to know what app she is looking at. The first kibele_analyze / kibele_flow / kibele_start call in a fresh session refuses with KBL_NO_PRODUCT_CONTEXT and a structured message telling the assistant to ask the user for:
- Category — fintech / dating / music / health / productivity / etc.
- Description — one or two sentences: what the app does, who it is for, what makes it different.
The assistant then calls kibele_set_context({ category, description }) and retries. The context lives in the MCP server process — every subsequent critique in the same session reuses it without re-asking. Restart Claude Code → fresh process → re-asked (this is intentional).
// First-time interaction, paraphrased:
//
// user: "kibele baksın" + screenshot
// assistant calls kibele_analyze({ image: ... })
// tool returns: KBL_NO_PRODUCT_CONTEXT — ask for category + description
// assistant: "Önce şunu sormam gerek: bu uygulama hangi kategori? Ne yapıyor, kim kullanıyor?"
// user: "Fintech. Avrupalı çiftler için ortak banka — fatura ve ortak hedefleri bölüştürürler."
// assistant calls kibele_set_context({ category: "fintech", description: "..." })
// assistant retries kibele_analyze({ image: ... })
// → critique runs
//
// Every later kibele_analyze in this session: no questions asked.Switching projects mid-session: call kibele_clear_context (or just kibele_set_context with new values).
Image input — three forms
Each tool that takes an image accepts ONE of:
// 1) Local file (most common in IDE workflow)
{ "image": { "image_path": "./screenshots/login.png" } }
// 2) Remote URL (Dribbble, Figma export, etc.)
{ "image": { "image_url": "https://example.com/screen.png" } }
// 3) Base64 (when user drag/drops into chat)
{ "image": { "image_base64": "iVBORw0KGgo...", "mime": "image/png" } }
// or with full data URL prefix:
{ "image": { "image_base64": "data:image/png;base64,iVBORw0KGgo..." } }Workflow examples
iOS dev — review LoginView
In Claude Code:
"Simulator'da LoginView açık, bir screenshot aldım
~/Desktop/login.png. Kibele baksın, bekleyebilirim."
Claude will call:
kibele_analyze({
image: { image_path: "/Users/you/Desktop/login.png" },
source_code: "<contents of LoginView.swift>",
product_profile_id: "my-fintech-app"
})Async — keep coding while Kibele deliberates
"Kibele bu ekrana baksın ama beklemeyelim, ben başka şeye geçiyorum"
kibele_start({
mode: "single",
image: { image_path: "./mock.png" },
label: "OTP screen v3"
})
// → { run_id: "a3f2b1c8", expected_seconds: "100-260" }Continue working. macOS notification fires when done. Then:
"Kibele ne dedi? runId a3f2b1c8"
kibele_result({ run_id: "a3f2b1c8" })
// → { status: "done", result: { structural: [...], editorial: [...] } }Drag-drop screenshot
When you drag a screenshot into Claude Code chat, the image is attached to the message. Claude can extract the base64 and pass it directly:
kibele_analyze({ image: { image_base64: "<extracted from dragged image>" } })Flow critique
kibele_flow({
steps: [
{ image: { image_path: "./01-login.png" }, label: "Login" },
{ image: { image_path: "./02-otp.png" }, label: "OTP" },
{ image: { image_path: "./03-kyc.png" }, label: "KYC" },
{ image: { image_path: "./04-home.png" }, label: "Home" }
],
flow_type: "auth"
})Async behaviour notes
kibele_start returns a runId in <1s, then runs the actual critique in the background of the MCP process. When it settles:
- Job state updates in-memory (queryable via
kibele_result). - JSON written to
~/.kibele-mcp/runs/<runId>.json(survives MCP restart for that one job). - macOS desktop notification fires (
osascript); other platforms append to~/.kibele-mcp/notifications.log.
The MCP protocol itself has no native async — the assistant is responsible for not auto-polling kibele_result in a loop. The tool descriptions say so explicitly. If a client misbehaves, the worst case is conversation blocks like the sync version would have.
In-memory job state is per-MCP-session. If you restart Claude Code mid-job, the runId is lost from the in-memory map but the persisted file at ~/.kibele-mcp/runs/<runId>.json still gets written when the in-flight HTTP call finishes (provided the MCP process survives — this depends on client lifecycle).
Environment variables
| Var | Default | Purpose |
|---|---|---|
| KIBELE_URL | http://localhost:3000 | Backend base URL |
| KIBELE_API_KEY | — | Sent as x-kibele-key header if set |
Limits
- 8 images max per single critique
- 12 steps max in a flow
- 20MB per image (PNG / JPEG / WebP / GIF)
These mirror the backend's enforcement.
Troubleshooting
kibele_health returns ok: false → Backend not running, or KIBELE_URL wrong. Run npm run start:dev in the repo root and check lsof -iTCP:3000 -sTCP:LISTEN.
Unauthorized → Backend has KIBELE_API_KEY set, MCP doesn't. Add KIBELE_API_KEY to the env block in your client config.
No notification on done → Check macOS System Settings → Notifications → Script Editor / osascript permission. Or tail -f ~/.kibele-mcp/notifications.log.
