Skip to content

Commit dd9376a

Browse files
Dumbrisclaude
andauthored
fix(intent): infer operation_type from tool variant instead of requiring it (#282)
* fix(intent): infer operation_type from tool variant instead of requiring it This fixes #278 where Gemini 3 Pro via Antigravity crashed when generating tool calls with nested JSON objects in the intent parameter. Changes: - Remove operation_type requirement from intent parameter - Infer operation_type automatically from tool variant: - call_tool_read → "read" - call_tool_write → "write" - call_tool_destructive → "destructive" - Make intent parameter optional (empty {} now works) - Update validateIntentForVariant to return (intent, error) and create default intent if nil - Simplify Validate() to only check optional fields (data_sensitivity, reason) - Update tests to reflect new behavior - Update documentation The two-key security model was redundant - the tool variant already declares intent. This simplification enables compatibility with models that have issues generating nested JSON objects. Closes #278 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(intent): remove intent object from schema for Gemini compatibility Gemini 3 Pro has strict limitations with nested objects in tool schemas: - Maximum 4-level nesting depth - Cannot handle untyped/schema-less objects - Strict schema validation Remove the `intent` object parameter entirely from call_tool_* schemas. The operation_type is already inferred from the tool variant, and the optional audit fields (data_sensitivity, reason) can be added back later if needed via flat string parameters. This should resolve the "improper format stop reason" errors when Gemini tries to generate tool calls. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(intent): flatten intent params for Gemini 3 Pro compatibility Replace nested intent object with flat string parameters to avoid Gemini 3 Pro's strict limitations with nested JSON objects: - Replace `intent: { data_sensitivity, reason }` with flat params: - `intent_data_sensitivity` - optional data classification - `intent_reason` - optional explanation for audit trail - Update extractIntent() to read from flat parameters - operation_type still inferred from tool variant (call_tool_read/write/destructive) - Activity log features preserved - intent metadata recorded in activity records - Update tests and documentation Fixes #278 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(cli): use flat intent params in call commands The CLI was still building a nested intent object which the server's extractIntent() no longer reads. Updated to use flat intent_data_sensitivity and intent_reason parameters. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(intent): improve intent param descriptions to encourage usage - Remove "Optional" from descriptions (models skip optional fields) - Change "For audit trail" to "Recommended for compliance/accountability" - Add "Requires intent.operation_type" to main tool descriptions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(intent): improve descriptions to encourage LLMs to provide intent fields - Remove operation_type mentions (inferred from tool name) - Make intent_data_sensitivity action-oriented: "Classify data being accessed/modified/deleted" - Make intent_reason a question with examples: "Why is this tool being called?" - Update retrieve_tools: "Always provide intent_reason and intent_data_sensitivity" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(lint): remove unused isValidOperationType function No longer needed since operation_type is inferred from tool variant. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f63c3f8 commit dd9376a

8 files changed

Lines changed: 275 additions & 356 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,10 @@ See [docs/configuration.md](docs/configuration.md) for complete reference.
173173

174174
**Tool Format**: `<serverName>:<toolName>` (e.g., `github:create_issue`)
175175

176-
**Intent Declaration (Spec 018)**: Tool variants enable granular IDE permission control. The `intent` parameter provides two-key security:
176+
**Intent Declaration (Spec 018)**: Tool variants enable granular IDE permission control. The `operation_type` is automatically inferred from the tool variant (`call_tool_read` → "read", etc.). Optional `intent` fields for audit:
177177
```json
178178
{
179179
"intent": {
180-
"operation_type": "read",
181180
"data_sensitivity": "public",
182181
"reason": "User requested list of repositories"
183182
}

cmd/mcpproxy/call_cmd.go

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -350,22 +350,17 @@ func runCallToolVariant(toolVariant, operationType string) error {
350350
return fmt.Errorf("invalid JSON arguments: %w", err)
351351
}
352352

353-
// Build intent declaration
354-
intent := map[string]interface{}{
355-
"operation_type": operationType,
353+
// Build arguments for the tool variant with flat intent params
354+
variantArgs := map[string]interface{}{
355+
"name": callToolName,
356+
"args": toolArgs,
356357
}
358+
// Add flat intent params (operation_type is inferred from tool variant)
357359
if callIntentSensitivity != "" {
358-
intent["data_sensitivity"] = callIntentSensitivity
360+
variantArgs["intent_data_sensitivity"] = callIntentSensitivity
359361
}
360362
if callIntentReason != "" {
361-
intent["reason"] = callIntentReason
362-
}
363-
364-
// Build arguments for the tool variant
365-
variantArgs := map[string]interface{}{
366-
"name": callToolName,
367-
"args": toolArgs,
368-
"intent": intent,
363+
variantArgs["intent_reason"] = callIntentReason
369364
}
370365

371366
// Load configuration

docs/features/intent-declaration.md

Lines changed: 49 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -42,58 +42,54 @@ MCPProxy Tools:
4242
[ ] call_tool_destructive → Always ask + confirm
4343
```
4444

45-
## Two-Key Security Model
45+
## How It Works
4646

47-
Agents must declare intent in **two places** that must match:
48-
49-
1. **Tool Selection**: Which variant to call (`call_tool_read` / `write` / `destructive`)
50-
2. **Intent Parameter**: `intent.operation_type` must match the tool variant
47+
The tool variant (`call_tool_read` / `write` / `destructive`) **automatically determines** the operation type. Intent metadata is provided as **flat string parameters** (not nested objects) for maximum compatibility with AI models:
5148

5249
```json
5350
{
5451
"name": "call_tool_destructive",
5552
"arguments": {
5653
"name": "github:delete_repo",
5754
"args_json": "{\"repo\": \"test-repo\"}",
58-
"intent": {
59-
"operation_type": "destructive",
60-
"data_sensitivity": "private",
61-
"reason": "User requested repository cleanup"
62-
}
55+
"intent_data_sensitivity": "private",
56+
"intent_reason": "User requested repository cleanup"
6357
}
6458
}
6559
```
6660

67-
**Why two keys?** This prevents:
68-
- Accidental misclassification (agent confusion)
69-
- Intentional misclassification (attack attempts)
70-
- Sneaking destructive operations through auto-approved read channel
61+
The `operation_type` is inferred from the tool variant - agents don't need to specify it explicitly.
7162

7263
### Validation Chain
7364

74-
1. Tool variant declares expected intent (`call_tool_destructive` expects "destructive")
75-
2. `intent.operation_type` is validated (MUST be "destructive")
76-
3. Mismatch → **REJECT** with clear error message
77-
4. Server annotation check → validate against `destructiveHint`/`readOnlyHint`
65+
1. Tool variant determines operation type (`call_tool_destructive` → "destructive")
66+
2. Optional intent fields (`intent_data_sensitivity`, `intent_reason`) are validated if provided
67+
3. Server annotation check → validate against `destructiveHint`/`readOnlyHint`
7868

7969
## Tool Variants
8070

8171
### call_tool_read
8272

8373
Execute read-only operations that don't modify state.
8474

75+
```json
76+
{
77+
"name": "github:list_repos",
78+
"args_json": "{\"org\": \"myorg\"}"
79+
}
80+
```
81+
82+
Or with optional metadata:
8583
```json
8684
{
8785
"name": "github:list_repos",
8886
"args_json": "{\"org\": \"myorg\"}",
89-
"intent": {
90-
"operation_type": "read"
91-
}
87+
"intent_reason": "Listing repositories for project analysis"
9288
}
9389
```
9490

9591
**Validation:**
96-
- `intent.operation_type` MUST be "read"
92+
- `operation_type` automatically inferred as "read"
9793
- Rejected if server marks tool as `destructiveHint: true`
9894

9995
### call_tool_write
@@ -104,15 +100,12 @@ Execute state-modifying operations that create or update resources.
104100
{
105101
"name": "github:create_issue",
106102
"args_json": "{\"title\": \"Bug report\", \"body\": \"Details...\"}",
107-
"intent": {
108-
"operation_type": "write",
109-
"reason": "Creating bug report per user request"
110-
}
103+
"intent_reason": "Creating bug report per user request"
111104
}
112105
```
113106

114107
**Validation:**
115-
- `intent.operation_type` MUST be "write"
108+
- `operation_type` automatically inferred as "write"
116109
- Rejected if server marks tool as `destructiveHint: true`
117110

118111
### call_tool_destructive
@@ -124,46 +117,43 @@ Execute destructive or irreversible operations.
124117
"name": "github:delete_repo",
125118
"args_json": "{\"repo\": \"test-repo\"}",
126119
"intent": {
127-
"operation_type": "destructive",
128-
"data_sensitivity": "private",
129-
"reason": "User confirmed deletion of test repository"
130-
}
120+
"intent_data_sensitivity": "private",
121+
"intent_reason": "User confirmed deletion of test repository"
131122
}
132123
```
133124

134125
**Validation:**
135-
- `intent.operation_type` MUST be "destructive"
126+
- `operation_type` automatically inferred as "destructive"
136127
- Most permissive - allowed regardless of server annotations
137128

138-
## Intent Parameter
129+
## Intent Parameters
130+
131+
Intent metadata is provided as **flat string parameters** for maximum compatibility with AI models (e.g., Gemini):
139132

140-
The `intent` object is **required** on all tool calls:
133+
| Parameter | Required | Values | Description |
134+
|-----------|----------|--------|-------------|
135+
| `intent_data_sensitivity` | No | `public`, `internal`, `private`, `unknown` | Data classification for audit |
136+
| `intent_reason` | No | String (max 1000 chars) | Explanation for audit trail |
141137

142-
| Field | Required | Values | Description |
143-
|-------|----------|--------|-------------|
144-
| `operation_type` | Yes | `read`, `write`, `destructive` | Must match tool variant |
145-
| `data_sensitivity` | No | `public`, `internal`, `private`, `unknown` | Data classification |
146-
| `reason` | No | String (max 1000 chars) | Explanation for audit trail |
138+
The `operation_type` is automatically inferred from the tool variant and cannot be overridden.
147139

148140
### Examples
149141

150-
**Minimal (required only):**
142+
**Minimal (no intent needed):**
151143
```json
152144
{
153-
"intent": {
154-
"operation_type": "read"
155-
}
145+
"name": "dataserver:read_data",
146+
"args_json": "{\"id\": \"123\"}"
156147
}
157148
```
158149

159-
**Full intent:**
150+
**With optional metadata:**
160151
```json
161152
{
162-
"intent": {
163-
"operation_type": "write",
164-
"data_sensitivity": "private",
165-
"reason": "Creating user profile with personal information"
166-
}
153+
"name": "dataserver:write_data",
154+
"args_json": "{\"id\": \"123\", \"value\": \"new\"}",
155+
"intent_data_sensitivity": "private",
156+
"intent_reason": "Updating user profile with personal information"
167157
}
168158
```
169159

@@ -284,22 +274,20 @@ curl -H "X-API-Key: $KEY" "http://127.0.0.1:8080/api/v1/activity?intent_type=des
284274

285275
Clear error messages help agents self-correct:
286276

287-
**Intent mismatch:**
288-
```
289-
Intent mismatch: tool is call_tool_read but intent declares write.
290-
Use call_tool_write for write operations.
291-
```
292-
293277
**Server annotation conflict:**
294278
```
295279
Tool 'github:delete_repo' is marked destructive by server.
296280
Use call_tool_destructive instead of call_tool_read.
297281
```
298282

299-
**Missing intent:**
283+
**Invalid data sensitivity:**
300284
```
301-
intent.operation_type is required.
302-
Provide intent: {operation_type: "read"|"write"|"destructive"}
285+
Invalid intent.data_sensitivity 'secret': must be public, internal, private, or unknown
286+
```
287+
288+
**Reason too long:**
289+
```
290+
intent.reason exceeds maximum length of 1000 characters
303291
```
304292

305293
## IDE Configuration Examples
@@ -362,14 +350,13 @@ The legacy `call_tool` has been removed. Update your integrations:
362350
"name": "call_tool_write",
363351
"arguments": {
364352
"name": "github:create_issue",
365-
"args_json": "{...}",
366-
"intent": {
367-
"operation_type": "write"
368-
}
353+
"args_json": "{...}"
369354
}
370355
}
371356
```
372357

358+
Intent parameters are optional - `operation_type` is automatically inferred from the tool variant. You can add `intent_data_sensitivity` and `intent_reason` for audit purposes.
359+
373360
:::tip Choosing the Right Variant
374361
When unsure, use `call_tool_destructive` - it's the most permissive and will always succeed validation. Then refine based on `retrieve_tools` guidance.
375362
:::

internal/contracts/intent.go

Lines changed: 16 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,13 @@ var ToolVariantToOperationType = map[string]string{
5555
const MaxReasonLength = 1000
5656

5757
// IntentDeclaration represents the agent's declared intent for a tool call.
58-
// This enables the two-key security model where intent must be declared both
59-
// in tool selection (call_tool_read/write/destructive) and in this parameter.
58+
// The operation_type is automatically inferred from the tool variant used
59+
// (call_tool_read/write/destructive), so agents only need to provide optional
60+
// metadata fields for audit and compliance purposes.
6061
type IntentDeclaration struct {
61-
// OperationType is REQUIRED and must match the tool variant used.
62+
// OperationType is automatically inferred from the tool variant.
6263
// Valid values: "read", "write", "destructive"
64+
// This field is populated by the server based on which tool variant is called.
6365
OperationType string `json:"operation_type"`
6466

6567
// DataSensitivity is optional classification of data being accessed/modified.
@@ -104,29 +106,9 @@ func NewIntentValidationError(code, message string, details map[string]interface
104106
}
105107
}
106108

107-
// Validate validates the IntentDeclaration fields
109+
// Validate validates the IntentDeclaration optional fields.
110+
// Note: operation_type is not validated here as it's inferred from tool variant.
108111
func (i *IntentDeclaration) Validate() *IntentValidationError {
109-
// Check operation_type is present
110-
if i.OperationType == "" {
111-
return NewIntentValidationError(
112-
IntentErrorCodeMissingOperationType,
113-
"intent.operation_type is required",
114-
nil,
115-
)
116-
}
117-
118-
// Check operation_type is valid
119-
if !isValidOperationType(i.OperationType) {
120-
return NewIntentValidationError(
121-
IntentErrorCodeInvalidOperationType,
122-
fmt.Sprintf("Invalid intent.operation_type '%s': must be read, write, or destructive", i.OperationType),
123-
map[string]interface{}{
124-
"provided": i.OperationType,
125-
"valid_values": ValidOperationTypes,
126-
},
127-
)
128-
}
129-
130112
// Check data_sensitivity if provided
131113
if i.DataSensitivity != "" && !isValidDataSensitivity(i.DataSensitivity) {
132114
return NewIntentValidationError(
@@ -154,15 +136,12 @@ func (i *IntentDeclaration) Validate() *IntentValidationError {
154136
return nil
155137
}
156138

157-
// ValidateForToolVariant validates that the intent matches the tool variant
139+
// ValidateForToolVariant validates the intent and sets operation_type from tool variant.
140+
// The operation_type is automatically inferred from the tool variant, so agents
141+
// don't need to provide it explicitly.
158142
func (i *IntentDeclaration) ValidateForToolVariant(toolVariant string) *IntentValidationError {
159-
// First validate the intent itself
160-
if err := i.Validate(); err != nil {
161-
return err
162-
}
163-
164-
// Get expected operation type for this tool variant
165-
expectedOpType, ok := ToolVariantToOperationType[toolVariant]
143+
// Get operation type for this tool variant
144+
opType, ok := ToolVariantToOperationType[toolVariant]
166145
if !ok {
167146
return NewIntentValidationError(
168147
IntentErrorCodeMismatch,
@@ -173,20 +152,11 @@ func (i *IntentDeclaration) ValidateForToolVariant(toolVariant string) *IntentVa
173152
)
174153
}
175154

176-
// Check two-key match: intent.operation_type must match tool variant
177-
if i.OperationType != expectedOpType {
178-
return NewIntentValidationError(
179-
IntentErrorCodeMismatch,
180-
fmt.Sprintf("Intent mismatch: tool is %s but intent declares %s", toolVariant, i.OperationType),
181-
map[string]interface{}{
182-
"tool_variant": toolVariant,
183-
"expected_operation": expectedOpType,
184-
"declared_operation": i.OperationType,
185-
},
186-
)
187-
}
155+
// Set operation_type from tool variant (inferring it)
156+
i.OperationType = opType
188157

189-
return nil
158+
// Validate the optional fields
159+
return i.Validate()
190160
}
191161

192162
// ValidateAgainstServerAnnotations validates intent against server-provided annotations
@@ -257,16 +227,6 @@ func DeriveCallWith(annotations *config.ToolAnnotations) string {
257227
return ToolVariantRead
258228
}
259229

260-
// isValidOperationType checks if the operation type is valid
261-
func isValidOperationType(opType string) bool {
262-
for _, valid := range ValidOperationTypes {
263-
if strings.EqualFold(opType, valid) {
264-
return opType == valid // Case-sensitive match required
265-
}
266-
}
267-
return false
268-
}
269-
270230
// isValidDataSensitivity checks if the data sensitivity is valid
271231
func isValidDataSensitivity(sensitivity string) bool {
272232
for _, valid := range ValidDataSensitivities {

0 commit comments

Comments
 (0)