Skip to content
3 changes: 1 addition & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,10 @@ See [docs/configuration.md](docs/configuration.md) for complete reference.

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

**Intent Declaration (Spec 018)**: Tool variants enable granular IDE permission control. The `intent` parameter provides two-key security:
**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:
```json
{
"intent": {
"operation_type": "read",
"data_sensitivity": "public",
"reason": "User requested list of repositories"
}
Expand Down
19 changes: 7 additions & 12 deletions cmd/mcpproxy/call_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,22 +350,17 @@ func runCallToolVariant(toolVariant, operationType string) error {
return fmt.Errorf("invalid JSON arguments: %w", err)
}

// Build intent declaration
intent := map[string]interface{}{
"operation_type": operationType,
// Build arguments for the tool variant with flat intent params
variantArgs := map[string]interface{}{
"name": callToolName,
"args": toolArgs,
}
// Add flat intent params (operation_type is inferred from tool variant)
if callIntentSensitivity != "" {
intent["data_sensitivity"] = callIntentSensitivity
variantArgs["intent_data_sensitivity"] = callIntentSensitivity
}
if callIntentReason != "" {
intent["reason"] = callIntentReason
}

// Build arguments for the tool variant
variantArgs := map[string]interface{}{
"name": callToolName,
"args": toolArgs,
"intent": intent,
variantArgs["intent_reason"] = callIntentReason
}

// Load configuration
Expand Down
111 changes: 49 additions & 62 deletions docs/features/intent-declaration.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,58 +42,54 @@ MCPProxy Tools:
[ ] call_tool_destructive → Always ask + confirm
```

## Two-Key Security Model
## How It Works

Agents must declare intent in **two places** that must match:

1. **Tool Selection**: Which variant to call (`call_tool_read` / `write` / `destructive`)
2. **Intent Parameter**: `intent.operation_type` must match the tool variant
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:

```json
{
"name": "call_tool_destructive",
"arguments": {
"name": "github:delete_repo",
"args_json": "{\"repo\": \"test-repo\"}",
"intent": {
"operation_type": "destructive",
"data_sensitivity": "private",
"reason": "User requested repository cleanup"
}
"intent_data_sensitivity": "private",
"intent_reason": "User requested repository cleanup"
}
}
```

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

### Validation Chain

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

## Tool Variants

### call_tool_read

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

```json
{
"name": "github:list_repos",
"args_json": "{\"org\": \"myorg\"}"
}
```

Or with optional metadata:
```json
{
"name": "github:list_repos",
"args_json": "{\"org\": \"myorg\"}",
"intent": {
"operation_type": "read"
}
"intent_reason": "Listing repositories for project analysis"
}
```

**Validation:**
- `intent.operation_type` MUST be "read"
- `operation_type` automatically inferred as "read"
- Rejected if server marks tool as `destructiveHint: true`

### call_tool_write
Expand All @@ -104,15 +100,12 @@ Execute state-modifying operations that create or update resources.
{
"name": "github:create_issue",
"args_json": "{\"title\": \"Bug report\", \"body\": \"Details...\"}",
"intent": {
"operation_type": "write",
"reason": "Creating bug report per user request"
}
"intent_reason": "Creating bug report per user request"
}
```

**Validation:**
- `intent.operation_type` MUST be "write"
- `operation_type` automatically inferred as "write"
- Rejected if server marks tool as `destructiveHint: true`

### call_tool_destructive
Expand All @@ -124,46 +117,43 @@ Execute destructive or irreversible operations.
"name": "github:delete_repo",
"args_json": "{\"repo\": \"test-repo\"}",
"intent": {
"operation_type": "destructive",
"data_sensitivity": "private",
"reason": "User confirmed deletion of test repository"
}
"intent_data_sensitivity": "private",
"intent_reason": "User confirmed deletion of test repository"
}
```

**Validation:**
- `intent.operation_type` MUST be "destructive"
- `operation_type` automatically inferred as "destructive"
- Most permissive - allowed regardless of server annotations

## Intent Parameter
## Intent Parameters

Intent metadata is provided as **flat string parameters** for maximum compatibility with AI models (e.g., Gemini):

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

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

### Examples

**Minimal (required only):**
**Minimal (no intent needed):**
```json
{
"intent": {
"operation_type": "read"
}
"name": "dataserver:read_data",
"args_json": "{\"id\": \"123\"}"
}
```

**Full intent:**
**With optional metadata:**
```json
{
"intent": {
"operation_type": "write",
"data_sensitivity": "private",
"reason": "Creating user profile with personal information"
}
"name": "dataserver:write_data",
"args_json": "{\"id\": \"123\", \"value\": \"new\"}",
"intent_data_sensitivity": "private",
"intent_reason": "Updating user profile with personal information"
}
```

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

Clear error messages help agents self-correct:

**Intent mismatch:**
```
Intent mismatch: tool is call_tool_read but intent declares write.
Use call_tool_write for write operations.
```

**Server annotation conflict:**
```
Tool 'github:delete_repo' is marked destructive by server.
Use call_tool_destructive instead of call_tool_read.
```

**Missing intent:**
**Invalid data sensitivity:**
```
intent.operation_type is required.
Provide intent: {operation_type: "read"|"write"|"destructive"}
Invalid intent.data_sensitivity 'secret': must be public, internal, private, or unknown
```

**Reason too long:**
```
intent.reason exceeds maximum length of 1000 characters
```

## IDE Configuration Examples
Expand Down Expand Up @@ -362,14 +350,13 @@ The legacy `call_tool` has been removed. Update your integrations:
"name": "call_tool_write",
"arguments": {
"name": "github:create_issue",
"args_json": "{...}",
"intent": {
"operation_type": "write"
}
"args_json": "{...}"
}
}
```

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.

:::tip Choosing the Right Variant
When unsure, use `call_tool_destructive` - it's the most permissive and will always succeed validation. Then refine based on `retrieve_tools` guidance.
:::
72 changes: 16 additions & 56 deletions internal/contracts/intent.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@ var ToolVariantToOperationType = map[string]string{
const MaxReasonLength = 1000

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

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

// Validate validates the IntentDeclaration fields
// Validate validates the IntentDeclaration optional fields.
// Note: operation_type is not validated here as it's inferred from tool variant.
func (i *IntentDeclaration) Validate() *IntentValidationError {
// Check operation_type is present
if i.OperationType == "" {
return NewIntentValidationError(
IntentErrorCodeMissingOperationType,
"intent.operation_type is required",
nil,
)
}

// Check operation_type is valid
if !isValidOperationType(i.OperationType) {
return NewIntentValidationError(
IntentErrorCodeInvalidOperationType,
fmt.Sprintf("Invalid intent.operation_type '%s': must be read, write, or destructive", i.OperationType),
map[string]interface{}{
"provided": i.OperationType,
"valid_values": ValidOperationTypes,
},
)
}

// Check data_sensitivity if provided
if i.DataSensitivity != "" && !isValidDataSensitivity(i.DataSensitivity) {
return NewIntentValidationError(
Expand Down Expand Up @@ -154,15 +136,12 @@ func (i *IntentDeclaration) Validate() *IntentValidationError {
return nil
}

// ValidateForToolVariant validates that the intent matches the tool variant
// ValidateForToolVariant validates the intent and sets operation_type from tool variant.
// The operation_type is automatically inferred from the tool variant, so agents
// don't need to provide it explicitly.
func (i *IntentDeclaration) ValidateForToolVariant(toolVariant string) *IntentValidationError {
// First validate the intent itself
if err := i.Validate(); err != nil {
return err
}

// Get expected operation type for this tool variant
expectedOpType, ok := ToolVariantToOperationType[toolVariant]
// Get operation type for this tool variant
opType, ok := ToolVariantToOperationType[toolVariant]
if !ok {
return NewIntentValidationError(
IntentErrorCodeMismatch,
Expand All @@ -173,20 +152,11 @@ func (i *IntentDeclaration) ValidateForToolVariant(toolVariant string) *IntentVa
)
}

// Check two-key match: intent.operation_type must match tool variant
if i.OperationType != expectedOpType {
return NewIntentValidationError(
IntentErrorCodeMismatch,
fmt.Sprintf("Intent mismatch: tool is %s but intent declares %s", toolVariant, i.OperationType),
map[string]interface{}{
"tool_variant": toolVariant,
"expected_operation": expectedOpType,
"declared_operation": i.OperationType,
},
)
}
// Set operation_type from tool variant (inferring it)
i.OperationType = opType

return nil
// Validate the optional fields
return i.Validate()
}

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

// isValidOperationType checks if the operation type is valid
func isValidOperationType(opType string) bool {
for _, valid := range ValidOperationTypes {
if strings.EqualFold(opType, valid) {
return opType == valid // Case-sensitive match required
}
}
return false
}

// isValidDataSensitivity checks if the data sensitivity is valid
func isValidDataSensitivity(sensitivity string) bool {
for _, valid := range ValidDataSensitivities {
Expand Down
Loading