Feature: 018-intent-declaration | Date: 2025-12-28
Extend existing handleCallTool() pattern to create three new handlers: handleCallToolRead(), handleCallToolWrite(), handleCallToolDestructive().
- Existing
handleCallTool()ininternal/server/mcp.go(line 762) provides proven pattern - Tool registration via
proxy.registerTools()(line 219) supports adding multiple tools - Built-in proxy tools use
proxyToolsmap for routing (line 798) - Parameter extraction via
request.RequireString()/request.GetString()is well-established
- Single handler with variant parameter: Rejected - defeats IDE permission model (single tool name)
- Middleware validation: Rejected - adds complexity without benefit
- Decorator pattern: Rejected - Go idiom favors explicit handlers
internal/server/mcp.go:762- Current handleCallTool implementationinternal/server/mcp.go:219- Tool registration pattern
Implement two-phase validation:
- Phase 1 (Two-Key Match): Validate
intent.operation_typematches tool variant - Phase 2 (Server Annotation): Validate against server's
destructiveHint/readOnlyHint
- Two-key model ensures agent explicitly declares intent twice (tool choice + parameter)
- Server annotation validation prevents intent spoofing via auto-approved channels
- Configurable strict mode allows flexibility for trusted environments
| Tool Variant | intent.operation_type | Server Annotation | Result |
|---|---|---|---|
| call_tool_read | "read" | destructiveHint=true | REJECT |
| call_tool_read | "read" | readOnlyHint=true | ALLOW |
| call_tool_read | "read" | no annotation | ALLOW |
| call_tool_read | "write" | any | REJECT (mismatch) |
| call_tool_write | "write" | destructiveHint=true | REJECT |
| call_tool_write | "write" | readOnlyHint=true | WARN + ALLOW |
| call_tool_write | "write" | no annotation | ALLOW |
| call_tool_destructive | "destructive" | any | ALLOW (most permissive) |
| any | missing | any | REJECT |
internal/config/config.go:358-365- Existing ToolAnnotations structinternal/contracts/converters.go:493-511- Annotation extraction
Store intent in existing ActivityRecord.Metadata field as structured object.
Metadata map[string]interface{}already exists in ActivityRecord (spec 016)- No schema migration needed - just populate the field
- Consistent with extensibility pattern already in place
Metadata: map[string]interface{}{
"intent": map[string]interface{}{
"operation_type": "read|write|destructive",
"data_sensitivity": "public|internal|private|unknown", // optional
"reason": "user provided reason", // optional
},
"tool_variant": "call_tool_read|call_tool_write|call_tool_destructive",
}internal/storage/activity_models.go- ActivityRecord structinternal/server/mcp.go:243-257- Activity event emission
Add three subcommands under mcpproxy call: tool-read, tool-write, tool-destructive.
- Matches existing
call toolpattern incmd/mcpproxy/call_cmd.go - Cobra subcommand structure is idiomatic
- Auto-populates
intent.operation_typebased on command used
mcpproxy call tool-read --tool-name=<server:tool> [--json_args JSON] [--reason TEXT] [--sensitivity LEVEL]
mcpproxy call tool-write --tool-name=<server:tool> [--json_args JSON] [--reason TEXT] [--sensitivity LEVEL]
mcpproxy call tool-destructive --tool-name=<server:tool> [--json_args JSON] [--reason TEXT] [--sensitivity LEVEL]- Single command with --intent flag: Rejected - doesn't match MCP tool split
- Separate mcpproxy read/write/delete commands: Rejected - breaks existing structure
cmd/mcpproxy/call_cmd.go- Existing call command
Add annotations and call_with fields to tool response, plus usage_instructions in summary.
- Agents need annotation visibility to select correct tool variant
call_withrecommendation simplifies agent logicusage_instructionsprovides context for new tools
{
"tools": [
{
"name": "github:delete_repo",
"description": "Delete a GitHub repository",
"inputSchema": {...},
"score": 0.95,
"server": "github",
"annotations": {
"readOnlyHint": false,
"destructiveHint": true
},
"call_with": "call_tool_destructive"
}
],
"usage_instructions": "Use call_tool_read for read-only operations, call_tool_write for modifications, call_tool_destructive for deletions. Intent must match tool variant."
}- If
destructiveHint=true→call_tool_destructive - If
readOnlyHint=true→call_tool_read - Otherwise →
call_tool_write(safe default)
internal/server/mcp.go:680-720- Tool list response construction
Add intent_declaration configuration block with strict_server_validation boolean.
- Matches existing config patterns in
internal/config/config.go - Single toggle for strict/permissive mode
- Default to strict (true) for security by default
{
"intent_declaration": {
"strict_server_validation": true
}
}true(default): Reject calls where intent doesn't match server annotationfalse: Log warning but allow call (for trusted environments)
internal/config/config.go- Configuration structs
Use clear, actionable error messages that explain what went wrong and how to fix it.
| Scenario | Error Message |
|---|---|
| Intent mismatch | Intent mismatch: tool is call_tool_read but intent declares write |
| Missing intent | intent parameter is required for call_tool_read |
| Missing operation_type | intent.operation_type is required |
| Server annotation mismatch | Tool 'github:delete_repo' is marked destructive by server, use call_tool_destructive |
| Unknown operation_type | Invalid intent.operation_type 'unknown': must be read, write, or destructive |
- Error messages explain the mismatch clearly
- Include the expected correction
- Match existing error patterns in codebase
Remove legacy call_tool entirely in this release. No deprecation period.
- Spec explicitly requires clean break (FR-004)
- Simplifies tool surface
- Eliminates ambiguity
- Forces explicit intent declaration
- Agents using
call_toolwill receive "tool not found" error - Error message should suggest using new variants
- Documentation clearly states breaking change
Tool 'call_tool' not found. Use call_tool_read, call_tool_write, or call_tool_destructive
with matching intent.operation_type. See retrieve_tools for annotations and recommendations.
Intent validation adds minimal overhead (<1ms) through in-memory operations only.
- Two-key validation: String comparison, O(1)
- Annotation lookup: Already cached in StateView, O(1)
- Config check: Single boolean read, O(1)
- No I/O: All validation happens before upstream call
- Total validation overhead: <10ms (spec SC-007)
- Expected actual: <1ms
Add intent_type query parameter to GET /api/v1/activity.
- Matches existing filter patterns (type, status, server, tool)
- Enables security audits of destructive operations
- Simple implementation via existing filter framework
GET /api/v1/activity?intent_type=destructive
GET /api/v1/activity?intent_type=read&status=error
internal/httpapi/server.go- Activity endpoint handleroas/swagger.yaml- OpenAPI spec