Skip to content

Commit 639bf10

Browse files
chughtapanclaude
andcommitted
feat(groups): Add structured JSON output for enable_tools/disable_tools
Replace text-formatted responses with Pydantic models (EnableToolsResult, DisableToolsResult) that FastMCP automatically serializes via ToolResult's structured_content parameter. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 81cca3c commit 639bf10

13 files changed

Lines changed: 2062 additions & 0 deletions

File tree

docs/middleware/groups.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Groups
2+
3+
When MCP servers expose many tools, agents can become overwhelmed with options, leading to poor tool selection and increased token usage. The `GroupsMiddleware` in <span class="wags-brand">wags</span> enables progressive tool disclosure by organizing tools into hierarchical groups that agents can enable or disable as needed.
4+
5+
## How It Works
6+
7+
Tools are assigned to groups using the `@in_group` decorator. The middleware:
8+
9+
1. Hides all grouped tools initially (or starts with configured `initial_groups`)
10+
2. Exposes `enable_tools` and `disable_tools` meta-tools
11+
3. Progressively reveals child groups as parents are enabled
12+
4. Enforces optional `max_tools` limits
13+
14+
## Example
15+
16+
```python linenums="1" title="handlers.py"
17+
from wags.middleware import GroupsMiddleware, GroupDefinition, in_group
18+
19+
class GithubHandlers:
20+
@in_group("issues")
21+
async def create_issue(self, owner: str, repo: str, title: str):
22+
pass
23+
24+
@in_group("issues")
25+
async def list_issues(self, owner: str, repo: str):
26+
pass
27+
28+
@in_group("repos")
29+
async def create_repository(self, name: str):
30+
pass
31+
```
32+
33+
Configure the middleware with group definitions:
34+
35+
```python title="main.py"
36+
from wags.middleware import GroupsMiddleware, GroupDefinition
37+
38+
handlers = GithubHandlers()
39+
mcp.add_middleware(
40+
GroupsMiddleware(
41+
groups={
42+
"issues": GroupDefinition(description="Issue tracking"),
43+
"repos": GroupDefinition(description="Repository management"),
44+
},
45+
handlers=handlers,
46+
initial_groups=["issues"], # Start with issues enabled
47+
max_tools=10, # Optional limit
48+
)
49+
)
50+
```
51+
52+
## Hierarchical Groups
53+
54+
Groups can be nested using the `parent` parameter for progressive disclosure:
55+
56+
```python
57+
groups = {
58+
"code": GroupDefinition(description="Code management"),
59+
"repos": GroupDefinition(description="Repositories", parent="code"),
60+
"branches": GroupDefinition(description="Branches", parent="repos"),
61+
}
62+
```
63+
64+
With this hierarchy:
65+
66+
- Only `code` is visible initially (root group)
67+
- Enabling `code` reveals `repos` as an option
68+
- Enabling `repos` reveals `branches` as an option
69+
- Disabling `code` cascades to disable `repos` and `branches`
70+
71+
## Agent Interaction
72+
73+
When an agent calls `enable_tools(groups=["issues"])`, it receives a structured JSON response:
74+
75+
```json
76+
{
77+
"enabled": ["issues"],
78+
"enabled_groups": ["issues"],
79+
"available_tools": ["create_issue", "list_issues"],
80+
"available_groups": [],
81+
"errors": []
82+
}
83+
```
84+
85+
The response includes:
86+
87+
- `enabled`: Groups that were newly enabled by this call
88+
- `enabled_groups`: All currently enabled groups
89+
- `available_tools`: Tools now available to the agent
90+
- `available_groups`: Child groups that can now be enabled
91+
- `errors`: Any validation errors (unknown groups, already enabled, etc.)
92+
93+
Similarly, `disable_tools` returns:
94+
95+
```json
96+
{
97+
"disabled": ["issues"],
98+
"enabled_groups": [],
99+
"available_tools": [],
100+
"errors": []
101+
}
102+
```
103+
104+
A `tools/list_changed` notification is sent whenever groups are enabled or disabled, prompting the client to refresh its tool list.
105+
106+
When a tool from a disabled group is called, the agent receives an error message with a hint about which group to enable.
107+
108+
## API Documentation
109+
110+
::: wags.middleware.groups.in_group
111+
options:
112+
show_source: false
113+
members: []
114+
show_signature: false
115+
116+
::: wags.middleware.groups.GroupDefinition
117+
options:
118+
show_source: false
119+
members: []
120+
show_signature: false
121+
122+
::: wags.middleware.groups.GroupsMiddleware
123+
options:
124+
show_source: false
125+
show_bases: false
126+
members: []
127+
show_signature: false
128+
129+
::: wags.middleware.groups.EnableToolsResult
130+
options:
131+
show_source: false
132+
members: []
133+
show_signature: false
134+
135+
::: wags.middleware.groups.DisableToolsResult
136+
options:
137+
show_source: false
138+
members: []
139+
show_signature: false

docs/middleware/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The <span class="wags-brand">wags</span> middleware toolkit further provides *fi
1717

1818
Understand what features different middlewares provide and how to configure them:
1919

20+
- [Groups](groups.md) for progressive tool disclosure via hierarchical groups.
2021
- [TodoList](todo.md) to ensure agents perform complex tasks correctly.
2122
- [Roots](roots.md) to enable client-configured fine-grained access control for MCP servers.
2223
- [Elicitation](elicitation.md) add human-in-the-loop features to improve UX.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ nav:
4141
- Onboarding Servers: onboarding.md
4242
- Middleware:
4343
- Overview: middleware/overview.md
44+
- Groups: middleware/groups.md
4445
- TodoList: middleware/todo.md
4546
- Roots: middleware/roots.md
4647
- Elicitation: middleware/elicitation.md

src/wags/middleware/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
"""WAGS middleware components."""
22

33
from .elicitation import ElicitationMiddleware, RequiresElicitation
4+
from .groups import (
5+
DisableToolsResult,
6+
EnableToolsResult,
7+
GroupDefinition,
8+
GroupsMiddleware,
9+
in_group,
10+
)
411
from .roots import RootsMiddleware, requires_root
512

613
__all__ = [
14+
"DisableToolsResult",
715
"ElicitationMiddleware",
16+
"EnableToolsResult",
17+
"GroupDefinition",
18+
"GroupsMiddleware",
819
"RequiresElicitation",
920
"RootsMiddleware",
21+
"in_group",
1022
"requires_root",
1123
]

0 commit comments

Comments
 (0)