feat(mcpd+deploy): serve web UI at /ui + smoke tests + docs (Stage 6)
Some checks failed
CI/CD / lint (pull_request) Successful in 54s
CI/CD / test (pull_request) Failing after 1m8s
CI/CD / typecheck (pull_request) Successful in 2m35s
CI/CD / smoke (pull_request) Has been skipped
CI/CD / build (pull_request) Has been skipped
CI/CD / publish (pull_request) Has been skipped
Some checks failed
CI/CD / lint (pull_request) Successful in 54s
CI/CD / test (pull_request) Failing after 1m8s
CI/CD / typecheck (pull_request) Successful in 2m35s
CI/CD / smoke (pull_request) Has been skipped
CI/CD / build (pull_request) Has been skipped
CI/CD / publish (pull_request) Has been skipped
The closing stage. mcpd now hosts the Stage 5 SPA, the Docker image bundles the build artifact, a smoke test exercises the personality HTTP surface end-to-end, and the user-facing docs spell out the mental model. mcpd: - Add @fastify/static dep. - New routes/web-ui.ts: registers /ui/* against a static bundle. Looks for the bundle at $MCPD_WEB_ROOT, then /usr/share/mcpd/web (the Docker image path), then a dev-tree fallback. Logs and skips cleanly if missing — API-only deploys keep working. - SPA fallback: any /ui/<path> that doesn't match a file falls through to index.html so direct hits to react-router URLs work. - /ui/* falls through to `kind: skip` in mapUrlToPermission, so the static assets are served unauthenticated. Each API call from the SPA still carries the bearer token. Deploy: - Dockerfile.mcpd builds the @mcpctl/web bundle in the same builder stage and copies dist/ to /usr/share/mcpd/web in the runtime image. Smoke (personality.smoke.test.ts): - Live mcpd flow: create secret/llm/agent/personality, attach an agent-direct prompt, verify the binding listing, reject double- attach (409) + foreign-agent prompt (400), set defaultPersonality by name, detach + delete cleanup. Docs: - New docs/personalities.md: VLAN-on-ethernet model, system-block ordering table, three prompt scopes, CLI walkthrough, web UI walkthrough, full API surface, RBAC notes. - agents.md and chat.md cross-link. - README's Agents section gains a Personalities subsection. Test count after Stage 6: mcpd: 801/801 cli: 430/430 web: 7/7 db: 58/62 (4 pre-existing) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
135
docs/personalities.md
Normal file
135
docs/personalities.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# 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 <name>` on the CLI (or `personality: "<name>"` 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=<name>
|
||||
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: <name>, priority? }
|
||||
# XOR with project: <name>
|
||||
```
|
||||
|
||||
Chat request body now accepts an optional `personality: "<name>"`. 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.
|
||||
Reference in New Issue
Block a user