# Personalities & agent-direct prompts A **personality** is a named overlay of prompts on top of an existing agent. Same agent, same LLM, same `systemPrompt` — but a different bundle of additional context injected at chat time. The mental model is a VLAN on top of ethernet: ethernet works on its own, and a VLAN tag adds segmentation without replacing the underlying link. Without a personality, an agent runs exactly as before. With one selected, its bound prompts get appended to the system block. ## What goes into the system block When you call an agent's chat endpoint, mcpd assembles the system block in this order (top wins by appearing first in the prompt): ``` agent.systemPrompt + agent-direct prompts (Prompt.agentId == agent.id, priority desc) + project prompts (Prompt.projectId == agent.projectId, priority desc) + personality-bound prompts (PersonalityPrompt[chosen], priority desc) + systemAppend (per-call override, --system-append) ``` Picking a personality is per-turn. Either: - pass `--personality ` on the CLI (or `personality: ""` in the chat request body), or - set `agent.defaultPersonalityId` on the agent — used when no `--personality` flag is given. Without either, today's behavior holds: agent + project prompts only. ## Three prompt scopes A `Prompt` row attaches to **at most one** of `projectId` or `agentId`. Every prompt fits exactly one of these slots: | Scope | `projectId` | `agentId` | Where it shows up | |----------------|-------------|-----------|------------------------------------------------------------| | Global | `null` | `null` | Any chat (passes the personality's "in scope" check) | | Project | set | `null` | All agents whose `projectId` matches | | Agent-direct | `null` | set | Only this agent. Always-on overlay, no toggle | Personality bindings (`PersonalityPrompt`) further select which of those prompts get injected when that personality is active. The service layer enforces a scope rule: a prompt can only be bound to a personality if it's already in-scope for that agent (agent-direct, agent's project, or global). Foreign-project prompts are rejected with HTTP 400. ## CLI ```fish # Make a personality on an existing agent mcpctl create personality grumpy --agent reviewer --description "Be terse and slightly grumpy" # Add an agent-direct prompt (always-on for this agent) mcpctl create prompt always-terse --agent reviewer --content "Always be terse." --priority 8 # Bind the prompt to the personality (HTTP for now — CLI subcommand to come) PERSONALITY_ID=$(mcpctl get personalities -o json | jq -r '.[] | select(.name=="grumpy") | .id') PROMPT_ID=$(mcpctl get prompts always-terse -o json | jq -r '.[0].id') curl -sf -H "Authorization: Bearer $(jq -r .token ~/.mcpctl/credentials)" \ -H "Content-Type: application/json" \ -X POST "https://mcpctl.ad.itaz.eu/api/v1/personalities/${PERSONALITY_ID}/prompts" \ -d "{\"promptId\": \"${PROMPT_ID}\", \"priority\": 9}" # Use it mcpctl chat reviewer --personality grumpy > what's wrong with this code? # Make it the default for this agent mcpctl edit agent reviewer # … in the YAML editor, set: # defaultPersonality: # name: grumpy ``` The chat banner shows which personality (if any) is active before the first prompt: ``` ──────────────────────────────────────────────────────────── Agent: reviewer — code review agent LLM: qwen3-thinking Project: code-quality Personality: grumpy (--personality) System prompt: You are a senior code reviewer. Be terse... ──────────────────────────────────────────────────────────── ``` ## Web UI The browser editor at `https://mcpctl.ad.itaz.eu/ui/` covers the same operations with Monaco for prompt editing: - **Projects → :name → prompts**: project-scoped prompt CRUD. - **Agents → :name → Direct prompts**: agent-direct prompt CRUD. - **Agents → :name → Personalities**: list, create, drill into a personality to bind/unbind prompts. The "attach prompt" picker only shows in-scope candidates (agent-direct, same-project, or global). The web UI uses the same bearer token as the CLI — paste a session token (`mcpctl auth login` writes one to `~/.mcpctl/credentials`) or mint a long-lived PAT (`mcpctl create mcptoken …`). The token is kept in `localStorage`; logout clears it. ## API surface ``` GET /api/v1/personalities GET /api/v1/personalities?agent= GET /api/v1/agents/:agentName/personalities POST /api/v1/agents/:agentName/personalities GET /api/v1/personalities/:id PUT /api/v1/personalities/:id DELETE /api/v1/personalities/:id GET /api/v1/personalities/:id/prompts POST /api/v1/personalities/:id/prompts body: { promptId, priority? } DELETE /api/v1/personalities/:id/prompts/:promptId GET /api/v1/agents/:agentName/prompts # agent-direct prompts only POST /api/v1/prompts # body: { name, content, agent: , priority? } # XOR with project: ``` Chat request body now accepts an optional `personality: ""`. RBAC: all personality endpoints inherit `agents:view/edit/create/delete`. There is no separate `personalities` resource in RBAC bindings — managing a personality is part of managing the parent agent. ## See also - [agents.md](./agents.md) — the parent resource. - [chat.md](./chat.md) — `mcpctl chat` flow + LiteLLM-style flags.