The AgentProject junction has existed since the data-model commit and nothing
read it. `grep -c agentProject src/mcpd/src/services/chat.service.ts` returned
0; chat resolved an agent's project from the legacy single `Agent.projectId`
scalar. So the schema claimed "an agent is a person who joins zero, one, or
many projects" (docs/agentic-teams.md §4) while the running system still gave
every agent exactly one project. A schema that lies about behaviour is worse
than a missing feature: it reads as done, so nobody builds it.
This makes the junction load-bearing.
Membership is now a real surface (task 152):
GET /api/v1/agents/:name/projects memberships
POST /api/v1/agents/:name/projects/:project join, body {role?, llm?}
DELETE /api/v1/agents/:name/projects/:project leave
GET /api/v1/projects/:name/memberships the roster, with roles
`role` is TEXT (developer|qa|security|sre in v1) and is validated in the
service layer, not the database — production provisions with `prisma db push`,
so a CHECK constraint written into a migration would never run there. Adding a
fifth role stays a one-line change.
RBAC: joining/leaving is `edit:agents:<name>`, not the `create:agents` the
default method→action mapping would have produced. Minting a whole new agent
and adding an existing one to a team are not the same authority; this mirrors
project↔server attach/detach already being `edit:projects`. Reading a roster is
`view:` on the side you asked from.
Chat honours it. `prepareContext` resolves the project from the membership when
the caller names one, and the resolution runs *before* the Llm lookup so a
per-membership `llmId` can replace the agent's pinned brain — and therefore its
whole failover pool — rather than patching the dispatch list afterwards. That
override is why the brain-ownership question (§15 Q1) can still go either way
without a migration: today the answer is per membership.
Naming a project the agent has not joined is an error that names the fix, not a
silent fallback. Answering from a *different* project's tools and prompts than
the one asked for is the failure mode worth being loud about.
The legacy scalar keeps working this release, as the schema comment promises:
`--project` accepts a membership *or* a legacy pin naming that same project,
bare `mcpctl chat <agent>` is unchanged, and `GET /projects/:p/agents` returns
the union of members and legacy-pinned agents so nothing disappears from a
project mid-migration (mcplocal's agents plugin reads that endpoint).
CLI: `mcpctl chat <agent> --project <p>` (previously rejected as "not both"),
plus `attach-agent` / `detach-agent` alongside the existing attach-server pair,
`get agents --project`, and `get memberships` with ROLE and BRAIN columns.
Completions come from the generator, not hand-edits.
Threads stay scoped to the agent, not to (agent, project) — §4 wants the
latter, but that needs a schema change and a story for what happens to those
threads when a membership is revoked. Deliberately not decided here.
Tests: 51 new (43 mcpd, 8 CLI). Full suite 2514 pass. Lint unchanged at
794 errors / 77 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.3 KiB
mcpctl chat
Open an interactive chat session with an Agent, or send a single message
in one shot. See agents.md for what an Agent is and how to
create one.
Modes
mcpctl chat <agent> # interactive REPL, new thread
mcpctl chat <agent> --project <p> # same agent, viewing project p
mcpctl chat <agent> --thread <id> # interactive REPL, resume thread
mcpctl chat <agent> -m "hi" # one-shot, prints reply, no REPL
mcpctl chat <agent> -m "hi" --no-stream # one-shot, single JSON response (no SSE)
Streaming is on by default. Text deltas land on stdout as they arrive; tool calls and tool results print to stderr in dim brackets so the chat output stays clean.
Two lenses on the same agent
mcpctl chat alice is the person on their own — their system prompt, their
own pinned brain, no project tools.
mcpctl chat alice --project sre is the same person viewing one of their
teams: the project's Prompts join the system block, the project's MCP servers
become callable tools, and if that membership carries a brain override, that
Llm answers instead of alice's pinned one.
mcpctl attach-agent alice --project sre --role sre # join first
mcpctl chat alice --project sre
The agent must actually be a member. Naming a project it has not joined is an error naming the fix, not a silent fallback to some other project's context:
Agent 'alice' is not a member of project 'payments'.
Join it with: mcpctl attach-agent alice --project payments
(The legacy single-project pin on the agent still counts as membership in that one project for this release — see agents.md.)
Threads stay scoped to the agent, not to (agent, project) — switching
--project mid-conversation changes the context layer, not the history.
Project chat
Chat directly with a project — no Agent needed. The session sees the
project's Prompts as system context, can call every tool from the project's
attached MCP servers, and uses the project's own LLM (llmProvider, with
llmModel as an optional served-model override).
mcpctl chat --project sre # REPL scoped to project "sre"
mcpctl chat --project sre -m "what alerts fired overnight?"
mcpctl chat --project sre --thread <id> # resume a past conversation
- Prompts + tools come from the project and stay current — anything you add to the project later shows up automatically in the next turn.
- LLM: set it once with
mcpctl patch project sre llmProvider=<llm-name>(and optionallyllmModel=<served-model>). Chat errors clearly if unset. - History is saved inside the project, attributed to you. List and resume:
Delete one you own (admins with
mcpctl get threads --project sre # your threads for this project mcpctl chat --project sre --thread <id>delete:projectscan delete anyone's):mcpctl delete thread <id> --project sre
Reading secrets (--allow-secrets)
By default the model cannot read secret values — it only benefits from them
indirectly (the project's tools run authenticated). Opt in with
--allow-secrets to expose a get_secret tool the model can call:
mcpctl chat --project sre --allow-secrets
This is gated: it requires the flag and your view:secrets permission,
and it is off by default. Secret values the model reads land in the chat
context and thread history, so use it deliberately.
RBAC: project chat and its threads route through run:projects:<name>.
Per-call flags
All optional. They override the agent's defaultParams for this session
only — use the in-REPL /save slash-command to persist the current set
back to the agent.
--system <text> # replace agent.systemPrompt for this session
--system-file <path> # read --system text from a file
--system-append <text> # append to the agent system block (after project Prompts)
--personality <name> # apply a personality overlay for this turn
# (additive — see docs/personalities.md)
--temperature <n> # 0..2
--top-p <n> # 0..1
--top-k <n> # integer; Anthropic-only, OpenAI ignores
--max-tokens <n> # cap on assistant tokens
--seed <n> # reproducibility (provider-dependent)
--stop <text> # stop sequence (repeatable, up to 4)
--allow-tool <name> # repeat to allowlist project MCP tools
--extra <key=value> # provider-specific knob (repeatable)
--no-stream # disable SSE; single JSON response
--extra is the LiteLLM-style escape hatch: pass anything the underlying
adapter understands. Numeric values are auto-parsed (--extra repetition_penalty=1.1); strings stay strings.
In-REPL slash-commands
/set KEY VALUE adjust an override for the rest of the session
(temperature, top-p, top-k, max-tokens, seed, stop,
or any provider-specific knob — unknown keys go
into `extra`)
/system <text> set systemAppend for this turn onward (empty = clear)
/tools list MCP servers the agent can call as tools
/clear start a fresh thread (same agent)
/save PATCH agent.defaultParams = current overrides
(systemOverride / systemAppend are NOT persisted)
/quit, /exit leave the REPL (Ctrl-D works too)
Threads
Threads persist server-side. To resume:
mcpctl get threads --agent reviewer
mcpctl chat reviewer --thread <id>
A mcpctl get thread <id> reads the message log:
mcpctl get thread c0abc… -o yaml
Examples
Quick gut-check on a deploy:
$ mcpctl chat reviewer -m "is fulldeploy.sh safe to run on the current branch?"
Yes — I checked: tests are green on commit 727e7d6 and there's no
in-flight migration. The k8s context is worker0-k8s0 (production); confirm
that's intended before running.
(thread: cm9k…)
Resuming with overrides:
$ mcpctl chat deployer --thread cm9k… --temperature 0.0 --max-tokens 256
> walk me through what changed since the last deploy
…
Pinning sampling defaults to the agent:
$ mcpctl chat deployer --temperature 0.0 --max-tokens 8000
> /save
(saved current overrides as agent.defaultParams)
> /quit
Troubleshooting
-
No agents appear in
tools/list— check the agent has a project attach (mcpctl describe agent <name>). The mcplocal plugin only exposes agents on their attached project's session. -
Tool calls fail with
Project not found— the agent has no project attach. Either attach it (mcpctl edit agent <name>and set the project field), or expect text-only chat. -
Anthropic agents can't call tools — known limitation; the Anthropic adapter doesn't translate OpenAI tool format yet. Use LiteLLM or a direct OpenAI-compatible provider for tool-using agents until the translator ships.
-
mcpctl chat <agent>returns 404 — the agent name doesn't resolve.mcpctl get agentsto confirm spelling. -
REPL feels stuck — agent tool calls can take minutes (e.g. running a Grafana query). Watch stderr for
[tool_call: …]/[tool_result: …]brackets; those tell you the loop is alive.