Grouper MCP Recipes
A recipe tells an AI client how you want a task done here — which tool to use, in what order, and what to avoid. This page is the deployer's reference: every setting, who can change what, and how a recipe quietly rewrites the description of other tools.
The problem it solves
An MCP client picks a tool by reading tool descriptions. If your institution has a local rule — use this GSH template, never that raw call; ask the owning team first — the client has no way to know. Put the rule in the general instructions and it competes with everything else in the conversation, and loses.
A recipe puts the rule where the client is actually looking: in the description of the tool it is about to call.
How a client actually receives one
This is the part that surprises people. A recipe reaches a client through three different channels, each carrying a different amount of it.
1. The recipe tool's own description. Grouper advertises a tool called recipe, and lists every recipe you can see — name and summary — inside its description. This is free: the client reads it whether or not it ever calls the tool.
2. Injected into other tools' descriptions. Every tool named in a recipe's toolNames gets a one-line pointer to that recipe appended to its own description. So a client choosing group_save reads your rule at the moment it chooses, without fetching anything. Only people who can see the recipe see the pointer — the same tool has a different description for different users.
3. The body, on demand. The full text is sent only when a client asks for the recipe by name with get, or when one of its toolNames tools fails. A long body therefore costs nothing until something actually needs it.
That layering is why the summary matters more than the body. The summary is sent to every client that can see the recipe, on every connection. The body is sent almost never.
Turning it on
Property | Scope | File |
|---|---|---|
| Global |
|
Note the file — this one lives in grouper.hibernate.properties, not grouper.properties, which catches people out. With MCP off, the recipes screen is hidden entirely.
Recipes are ordinary Grouper configuration, so they can ship as built-ins, be overridden by setting the same keys, and be turned off with enabled = false without losing them. Edit them in the UI under Miscellaneous → MCP recipes rather than by hand.
Who can do what
Two separate admin groups, and they are not interchangeable. Everything below is stated in terms of these four, because "admin" on its own is ambiguous here.
Setting | Scope | Grants |
|---|---|---|
| Global | Administers recipes through the UI screens: add, edit every field, delete. Grants nothing over MCP — a member is an ordinary MCP user there. |
| Global | Reads and updates any recipe over MCP, including the fields that decide who it reaches. Also confers UI administration — this one is the superset. Requires MCP readwrite on top. |
| Per recipe | Who can see and read the recipe. There is no global switch for this. |
| Per recipe | Who owns the wording. Members need not administer anything. |
The UI/MCP split is the point: you can let someone curate recipes at a keyboard without their AI client gaining any new reach.
No root fallback. Being a Grouper sysadmin, wheel, or root grants none of this. The checks are pure group membership. Until one of the admin properties names a group, nobody can administer recipes and the UI link is hidden from everyone — including
GrouperSystem. It is a guardrail against accidents, not a security boundary: anyone who can edit Grouper config can grant themselves the group.
In the UI
Action | UI-admin group | MCP-admin group | Recipe's edit group |
|---|---|---|---|
See the recipes screen | yes | yes | yes |
See every recipe listed | yes | yes | only its own |
Create | yes | yes | no |
Edit name, summary, body | yes | yes | yes |
Edit enabled, groups, toolNames, priority | yes | yes | read only |
Delete | yes | yes | no |
There is one edit screen, not two. A content owner opens the same form an administrator does and finds the fields they may not change rendered read-only — and the server ignores them on submit rather than trusting the posted form. allowEditInMcp is irrelevant in the UI; an edit-group member can always fix wording here.
Over MCP
Three actions exist: list, get, update. There is deliberately no create and no delete over MCP, for anyone.
Action | Who |
|
|---|---|---|
| MCP-admin group with readwrite sees everything; everyone else sees only recipes whose use group includes them | no |
| MCP-admin group, plus MCP readwrite | no — admins bypass the switch |
| the recipe's edit group, plus MCP readwrite | yes |
create / delete | nobody | — |
A delegated editor who sends a control field gets an explicit refusal naming that field — refused rather than silently dropped, so a client can't believe it changed something it didn't. The update action is only advertised at all when you can actually use it.
Property | Scope | Default |
|---|---|---|
| Global |
|
Whether the per-recipe groupNameCanEdit delegation works over MCP at all. Off by default, so a deployer opens that path once rather than blanking groupNameCanEdit on every recipe to close it. Turning it off does not strand anyone — the UI screen still works.
It is not a master off-switch. The check is
if (!canAdmin && !isAllowEditInMcp()), so members of the MCP-admin group skip it entirely: withallowEditInMcpset tofalsethey can still rewrite any recipe over MCP, includinggroupNameCanUseandtoolNames. To stop all editing over MCP you have to emptygrouper.mcp.recipe.groupNameCanAdminInMcpas well.
Per-recipe settings
Every key is prefixed grouperMcpRecipe.<configId>. — the config id is your handle for the configuration, and is not what the client sees.
Key | Required | Notes |
|---|---|---|
| yes | What the client asks for the recipe by, e.g. |
| yes | One line stating the rule. The most valuable string in the whole feature — see below. |
| yes | The detail: what order to do things in, what must be true first, edge cases, what to avoid. Don't repeat the summary. Sent only on |
| yes | Fails closed. Blank, or naming a group that can't be found, means nobody sees the recipe. Resolved in a root session, so it can name a group the recipe's audience cannot themselves view. |
| no | Blank means nobody owns the wording. Members of this group can change the name, summary and body — nothing else. Who the recipe reaches, which tools it applies to, whether it's enabled and its priority all stay with recipe administrators. |
| no | Comma-separated MCP tool names, e.g. |
| no (default | Turn a recipe off without deleting it. A disabled recipe is shown to no client — but stays editable, so you can take one down and rewrite it. |
| no (default | Lower comes first. Decides which recipes get their full summary rather than just their name when there are more than fit, and the order of pointers on a tool that several recipes apply to. The default sits mid-range so you can move one either way without renumbering. Ties break by name, so the same config always produces the same output. |
| read only | Written by Grouper on every edit through the screens or over MCP, and sent to the client alongside the body so a recipe arrives as local advice with a name on it. |
Writing a summary that works
Say what to do, not when the rule applies.
| Summary |
|---|---|
Good | Use the payrollGroupSave template for groups under app:payroll, not group_save |
Weaker | Applies to groups under app:payroll |
Worst | Payroll group operations |
The first lets the client act immediately. The second only says that something applies, so it must open the full recipe first — and often it won't bother. The third gives it nothing to act on.
Keep it to one line. Every client that can see the recipe receives this text on every connection.
Caching, and why your change hasn't appeared
Three separate delays sit between an edit and a client seeing it.
Property | Default | Notes |
|---|---|---|
|
| Recipes and per-subject permission answers are cached this long. A group membership change can take two minutes to affect recipe behaviour. Setting it to 0 or less turns caching off, which is useful while working out why a recipe is or isn't appearing, and wasteful left on. |
|
| Per-user entries grow with the number of distinct people on a node, so the cache is bounded by count as well as age. |
The other two delays:
Config file reload —
grouper.config.secondsBetweenUpdateChecksdefaults to 600. Agrouper.propertiesedit can take ten minutes. Database config, which is what the UI writes, doesn't wait on this.The client's tool list — MCP advertises tools once, at connection. A client connected before you changed a recipe keeps serving its user the old summary, and won't see a newly-permitted action, until it reconnects.
When testing permissions, restart Tomcat rather than waiting. It clears the caches outright and takes less time than reasoning about which one bit you.
A worked example
Written as properties; the UI writes the same keys to database config.
grouperMcpRecipe.payrollGroups.enabled = true
grouperMcpRecipe.payrollGroups.name = payroll-groups
grouperMcpRecipe.payrollGroups.summary = Groups under app:payroll are owned by the \
payroll team. Ask them before you change membership, and never delete one.
grouperMcpRecipe.payrollGroups.body = Groups under app:payroll drive pay runs, \
so a wrong membership has a real world effect...
grouperMcpRecipe.payrollGroups.groupNameCanUse = app:payroll:etc:payrollStaff
grouperMcpRecipe.payrollGroups.groupNameCanEdit = app:payroll:etc:payrollOwners
grouperMcpRecipe.payrollGroups.toolNames = group_save, group_delete, group_add_member
grouperMcpRecipe.payrollGroups.priority = 100The effect: members of payrollStaff see the recipe listed by their client, and see a pointer to it appended to the descriptions of group_save, group_delete and group_add_member. Members of payrollOwners can additionally rewrite the wording — in the UI always, over MCP only if allowEditInMcp is on. Nobody else sees that the recipe exists.
Things worth knowing before you deploy
There is no staging state. Set a use group and the recipe is live to those clients. To hold one back, use
enabled = falseor leave the use group blank — both also hide it from the UI list.Recipe text reaches AI clients verbatim. Treat the body as published to everyone in the use group. Don't put anything there you wouldn't want pulled into a model's context.
The advertised schema is not the boundary. A client can attempt actions and fields the tool description never offered it; the server-side checks are what actually refuse them.
A recipe can point at nothing. A tool renamed by an upgrade, or a deleted group, leaves a recipe that looks fine and does nothing. The recipes screen reports these under "Some recipes are not doing anything" — it's the only place they surface.