MCP Tool Profiles¶
Authoritative reference for which MCP tools each agent kind can call, derived from the real gating code in tatara-cli (internal/mcp/profiles.go, server.go, tools.go) and the pod-injection code in tatara-operator (internal/agent/pod.go). This supersedes any per-kind tool tables in other docs if they disagree - re-derive from the source files listed under Where profiles live rather than trusting a second copy.
How gating works¶
- The operator (
tatara-operator,internal/agent/pod.go) setsTATARA_TOOL_PROFILEin the agent pod's env, derived from the Task'sspec.kindviatoolProfileForKind. This is the only place the env var is actually assigned in the running platform. tatara mcp(the CLI,internal/cmd/mcp.go) readsTATARA_TOOL_PROFILE(or--tool-profile) and passes it intomcp.NewServer.tools/listis profile-invariant (Component 4a / G15):NewServerregisters every tool, for every profile, unconditionally. This keeps thetools/listresponse byte-identical across all agent kinds so every pod shares one Anthropic prompt-cache prefix - a per-kind filtered list would fragment the cache (tools render first in cache order).- Gating is enforced at call time instead, in the
register()dispatch closure: before invoking a tool's handler, the server checks the resolved per-profile allow-set. A denied call never reaches the backend; it returnstool "<name>" is not permitted for profile "<profile>"and incrementstool_calls_total{status="denied"}. - Fail-open:
TATARA_TOOL_PROFILEunset/empty ->allow == nil-> every registered tool is callable. This is the local-dev / no-profile path and logs a WARN. - Fail-closed: a non-empty but unrecognized profile string resolves to the
alwaysOnset only (4 tools) and logs a WARN. Profile gating is the sole authz boundary here - a typo must never silently grant the full surface. - Identity note: every agent pod authenticates as the same OIDC client (
tatara-agent). Authorization cannot key on caller identity, so it keys on tool profile plus env-scoped task/project context (TATARA_TASK,TATARA_PROJECT) instead.
Tool groups¶
72 tools are registered in total, from five Go functions in tatara-cli/internal/mcp/tools.go:
| Group | Count | Function | Target backend |
|---|---|---|---|
| Memory | 13 | AllTools() (memory half) | tatara-memory |
| Code-graph | 19 | AllTools() (code_* half) | tatara-memory |
| Operator | 25 | OperatorTools() | tatara-operator |
| Chat | 10 | ChatTools() | tatara-chat |
| Handoff | 4 | HandoffTools() | tatara-chat |
| Platform | 1 | PlatformTools() | local (no backend call) |
Memory (create_memory, get_memory, delete_memory, bulk_create_memories, get_ingest_job, query, describe, get_entity, search_entities, patch_entity, list_edges, create_edge, delete_edge) and code-graph (code_search, code_entity, code_neighbors, code_callers, code_callees, code_dependents, code_dependencies, code_file_imports, code_resource_graph, code_cross_repo, code_path, code_important, code_stats, code_ambiguous_edges, code_explain, code_related, code_hyperedges, code_communities, code_bridges) are unconditional: every named profile gets all 32 of them, unfiltered.
alwaysOn (all 8 named profiles, plus fail-closed unknown)¶
| Tool | Description |
|---|---|
report_internal_issue | Report a platform-internal issue (tool error, directive contradiction, workspace/memory/graph inconsistency, auth). Structured log + metric only, no SCM issue. |
project_get | Read the current project |
repo_list | List repositories in a project |
task_get | Read the current task |
Chat (10) - chat flag profiles only¶
chat_create_room, chat_list_rooms, chat_get_room, chat_close_room, chat_add_participant, chat_list_participants, chat_remove_participant, chat_send_message, chat_poll_messages, chat_get_log.
Handoff (4) - continuity-carrying profiles only¶
Added after the tool surface was last widely documented; not yet reflected everywhere else in the docs tree.
| Tool | Gate |
|---|---|
write_handoff | handoff flag profiles |
get_handoff | handoff flag profiles |
list_handoffs | handoff flag profiles |
delete_handoff | handoffDelete flag - refine only. Refine is the handoff groomer; every other continuity-carrying profile can write/read but never delete another pod's handoff. |
Per-kind profile table¶
toolProfileForKind maps a Task kind to a profile name (note triageIssue -> triage and issueLifecycle -> lifecycle; the CRD kind and the MCP profile name diverge for these two).
| Profile | Task kind(s) | Chat | Handoff (r/w) | Handoff delete | Operator tools | Total allowed |
|---|---|---|---|---|---|---|
refine | refine | no | yes | yes | 6 | 46 |
brainstorm | brainstorm, healthCheck | yes | yes | no | 7 | 56 |
implement | implement | no | yes | no | 8 | 47 |
review | review | no | no | no | 4 | 40 |
triage | triageIssue | no | no | no | 8 | 44 |
lifecycle | issueLifecycle | yes | yes | no | 14 | 63 |
incident | incident | yes | yes | no | 10 | 59 |
selfImprove | selfImprove | no | no | no | 9 | 45 |
| (empty) | fail-open, local dev | - | - | - | all 22 (+project_list) | 72 |
| (unrecognized string) | fail-closed | no | no | no | 0 | 4 |
Total = 32 (memory + code-graph, unconditional) + chat (0 or 10) + handoff (0, 3, or 4) + profile-specific operator tools + 4 (alwaysOn). healthCheck shares Kind and profile with brainstorm per the operator ("same goal, same tools").
lifecycle is the largest named profile (63 tools) because it is a single long-lived pod spanning Triage -> Conversation -> Implement -> MRCI -> Merge; per-lifecycle-state gating is infeasible without restarting the MCP server, so it gets the union of every other profile's operator tools.
refine's 6-tool operator surface is deliberately deny-by-default: it omits create_issue (issue creation is an escalation vector) and every SCM-mutation or lifecycle-escalation tool. Historical notes elsewhere describe refine as "~42 tools" - that count predates the handoff group being added (32 + 6 + 4 = 42); with handoff + delete_handoff now included, refine is 46.
project_list and create_issue are registered (OperatorTools()) but not granted to any named profile - they are reachable only in fail-open mode (empty TATARA_TOOL_PROFILE, e.g. local dev without the operator injecting a profile).
Operator tool matrix¶
All 25 OperatorTools() (project_get, repo_list, task_get are the alwaysOn 3 of these and are omitted below since every profile has them).
| Tool | refine | brainstorm | implement | review | triage | lifecycle | incident | selfImprove |
|---|---|---|---|---|---|---|---|---|
project_list | ||||||||
task_list | x | x | x | x | x | |||
task_update | x | x | x | x | x | x | ||
subtask_list | x | x | x | x | x | x | x | |
subtask_create | x | x | x | x | x | x | ||
subtask_update | x | x | x | x | x | x | ||
propose_issue | x | x | ||||||
review_verdict | x | x | ||||||
pr_outcome | x | x | ||||||
change_summary | x | x | x | x | ||||
submit_handover | x | x | x | x | x | |||
issue_outcome | x | x | ||||||
decline_implementation | x | x | x | x | ||||
already_done | x | x | x | |||||
skip_research | x | |||||||
comment | x | x | ||||||
list_issues | x | |||||||
list_commits | x | |||||||
close_issue | x | |||||||
edit_issue | x | |||||||
create_issue | ||||||||
comment_on_issue | x | x | x | x | x |
By category¶
| Category | Tools |
|---|---|
| Task/subtask ledger | task_list, task_get*, task_update, subtask_list, subtask_create, subtask_update |
| Project/repo | project_list (fail-open only), project_get*, repo_list* |
| Issues (SCM) | propose_issue, comment_on_issue, comment, list_issues, close_issue, edit_issue, create_issue (fail-open only), issue_outcome |
| PR/MR | review_verdict, pr_outcome, change_summary |
| Refusal/terminal outcome | decline_implementation, already_done, skip_research, submit_handover |
| Platform self-report | report_internal_issue* |
| Memory/graph | see Tool groups, 32 tools, unconditional |
| Conversation (agent-to-agent) | chat_* (10), conditional on chat |
| Continuity | write_handoff, get_handoff, list_handoffs, delete_handoff (refine only) |
* alwaysOn - present in every profile including fail-closed unknown.
Notable fields¶
change_summaryrequireschange_significance(major|minor|patch) as of the semver push-CD cutover - the tool call is rejected server-side if omitted or not one of the three enum values. This is what drives the auto-merge -> semver tag ->tatara-helmfilecascade; humans set the equivalent via asemver:<level>PR label.edit_issuepatches onlytitleandbody. Labels are intentionally not editable through this tool - issue labels drive the lifecycle state machine and stay operator/maintainer-controlled, so no profile (includingrefine, the groomer) can set them.decline_implementation/already_done/skip_researchall require a non-emptyreason; a silent finish with no PR and no outcome call is rejected as an unexplained refusal.
Where profiles live¶
| What | File | Notes |
|---|---|---|
Profile -> tool-set definitions (groupMemory, groupCodeGraph, groupChat, groupHandoff, groupHandoffDelete, alwaysOn, profiles map, resolveProfile) | tatara-cli/internal/mcp/profiles.go (whole file, ~253 lines) | Source of truth for what each profile allows. |
| Tool registration + call-time authz | tatara-cli/internal/mcp/server.go (NewServer ~L36-73, register() dispatch closure ~L112-165) | Registers all tools unconditionally; gates at call time per Component 4a. |
| Tool definitions (name/description/schema/handler per group) | tatara-cli/internal/mcp/tools.go: AllTools() L45, OperatorTools() L334, ChatTools() L788, HandoffTools() L933, PlatformTools() L1016 | Edit here to add/rename a tool or change its schema. |
| CLI flag/env wiring | tatara-cli/internal/cmd/mcp.go (tatara mcp command, ~L145 reads TATARA_TOOL_PROFILE/--tool-profile) | |
| Kind -> profile mapping used by the CLI's own tests | tatara-cli/internal/mcp/profiles.go toolProfileForKind (top of file) | Test-locked mirror of the operator's mapping; not called by the running tatara mcp serve path itself. |
| Kind -> profile mapping actually applied to pods | tatara-operator/internal/agent/pod.go: env injection at L655, toolProfileForKind at L838 | This is the authoritative mapping - the operator, not the CLI, decides which profile a pod gets. |
How to change a kind's tool surface¶
- To change which tools a profile grants: edit the
profilesmap (or agroup*slice) intatara-cli/internal/mcp/profiles.go. - To add a new tool: add it to the relevant
*Tools()function intatara-cli/internal/mcp/tools.go, then add its name to the profile(s) that should get it inprofiles.go. - To remap which profile a Task kind gets: edit
toolProfileForKindintatara-operator/internal/agent/pod.go(this is the one that matters at runtime; keep thetatara-clicopy in sync since its test asserts the same mapping contract). - Both repos deploy via the standard semver push-CD path: merge to
mainwith the requiredchange_significanceonchange_summary(or asemver:<level>PR label for a human change) -> CI builds and tags ->tatara-helmfileauto-applies the new image/chart pin to the cluster. Never hand-edit a deploy pin; atatara-clichange also needs thetatara-claude-code-wrapperimage'sTATARA_CLI_VERSIONbumped forward before agent pods pick it up.