Skip to content

Commit a0cdc37

Browse files
Dumbrisclaude
andauthored
feat: Enhanced Tool Annotations Intelligence (Spec 035) (#342)
* feat: add GET /api/v1/annotations/coverage endpoint Add annotation coverage reporting endpoint that shows how many upstream tools have MCP annotations (hint booleans) vs those that don't, broken down by server. A tool counts as annotated only if at least one of ReadOnlyHint, DestructiveHint, IdempotentHint, or OpenWorldHint is set (Title alone does not count). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: regenerate OpenAPI spec for annotations/coverage endpoint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add lethal trifecta session risk analysis and annotation-based filtering to retrieve_tools (Spec 035 F2+F4) F2: Session risk analysis examines all connected servers' tool annotations to detect the "lethal trifecta" — open-world access + destructive capabilities + write access. Returns risk level (high/medium/low) in every retrieve_tools response as session_risk, with a warning when the trifecta is present. F4: Three new optional boolean parameters (read_only_only, exclude_destructive, exclude_open_world) allow agents to self-restrict tool discovery scope based on MCP annotation hints. Nil annotations are treated as most permissive per spec. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add openWorldHint content trust scanning for tool calls (Spec 035 F3) When a tool call is made via call_tool_read/write/destructive, code_execution, or direct routing mode, check if the called tool has openWorldHint=true (or nil, which defaults to true per MCP spec). Tag the activity record metadata with "content_trust": "untrusted" for open-world tools, or "trusted" for closed-world tools (openWorldHint=false). This enables downstream security review of tool outputs that may contain untrusted external data. Changes: - Add IsOpenWorldTool() and ContentTrustForTool() helpers in contracts/intent.go - Add content_trust field to EmitActivityToolCallCompleted event payload - Add content_trust extraction in handleToolCallCompleted and handleInternalToolCall activity service handlers - Add EmitActivityInternalToolCallWithContentTrust for code_execution path - Compute content trust in handleCallToolVariant, makeDirectModeHandler, and code_execution handler (any open-world tool call marks entire execution) - Add comprehensive tests: TestIsOpenWorldTool, TestContentTrustForTool, TestHandleToolCallCompleted_ContentTrust, TestHandleInternalToolCall_ContentTrust Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add Spec 035 Enhanced Tool Annotations Intelligence Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: merge duplicated EmitActivityInternalToolCall functions Consolidate EmitActivityInternalToolCallWithContentTrust into EmitActivityInternalToolCall by adding contentTrust parameter to the original function. Eliminates 35-line copy-paste duplication. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com>
1 parent 13b9386 commit a0cdc37

15 files changed

Lines changed: 1400 additions & 73 deletions

File tree

internal/contracts/intent.go

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,20 @@ type IntentDeclaration struct {
7676

7777
// IntentValidationError represents intent validation failures
7878
type IntentValidationError struct {
79-
Code string `json:"code"` // Error code for programmatic handling
80-
Message string `json:"message"` // Human-readable error message
81-
Details map[string]interface{} `json:"details" swaggertype:"object"` // Additional context
79+
Code string `json:"code"` // Error code for programmatic handling
80+
Message string `json:"message"` // Human-readable error message
81+
Details map[string]interface{} `json:"details" swaggertype:"object"` // Additional context
8282
}
8383

8484
// Error codes for intent validation
8585
const (
86-
IntentErrorCodeMissing = "MISSING_INTENT"
86+
IntentErrorCodeMissing = "MISSING_INTENT"
8787
IntentErrorCodeMissingOperationType = "MISSING_OPERATION_TYPE"
8888
IntentErrorCodeInvalidOperationType = "INVALID_OPERATION_TYPE"
89-
IntentErrorCodeMismatch = "INTENT_MISMATCH"
90-
IntentErrorCodeServerMismatch = "SERVER_MISMATCH"
91-
IntentErrorCodeInvalidSensitivity = "INVALID_SENSITIVITY"
92-
IntentErrorCodeReasonTooLong = "REASON_TOO_LONG"
89+
IntentErrorCodeMismatch = "INTENT_MISMATCH"
90+
IntentErrorCodeServerMismatch = "SERVER_MISMATCH"
91+
IntentErrorCodeInvalidSensitivity = "INVALID_SENSITIVITY"
92+
IntentErrorCodeReasonTooLong = "REASON_TOO_LONG"
9393
)
9494

9595
// Error implements the error interface
@@ -115,8 +115,8 @@ func (i *IntentDeclaration) Validate() *IntentValidationError {
115115
IntentErrorCodeInvalidSensitivity,
116116
fmt.Sprintf("Invalid intent.data_sensitivity '%s': must be public, internal, private, or unknown", i.DataSensitivity),
117117
map[string]interface{}{
118-
"provided": i.DataSensitivity,
119-
"valid_values": ValidDataSensitivities,
118+
"provided": i.DataSensitivity,
119+
"valid_values": ValidDataSensitivities,
120120
},
121121
)
122122
}
@@ -227,6 +227,36 @@ func DeriveCallWith(annotations *config.ToolAnnotations) string {
227227
return ToolVariantRead
228228
}
229229

230+
// Content trust constants for open-world hint scanning (Spec 035)
231+
const (
232+
// ContentTrustUntrusted marks tool output as untrusted (open-world tool, data from external sources)
233+
ContentTrustUntrusted = "untrusted"
234+
// ContentTrustTrusted marks tool output as trusted (closed-world tool, data from controlled sources)
235+
ContentTrustTrusted = "trusted"
236+
)
237+
238+
// IsOpenWorldTool checks whether a tool's annotations indicate it operates in an
239+
// open-world context (fetching data from external/untrusted sources). Per the MCP spec,
240+
// openWorldHint defaults to true when nil or when annotations are nil entirely.
241+
func IsOpenWorldTool(annotations *config.ToolAnnotations) bool {
242+
if annotations == nil {
243+
return true // spec default: openWorldHint=true
244+
}
245+
if annotations.OpenWorldHint == nil {
246+
return true // nil defaults to true per spec
247+
}
248+
return *annotations.OpenWorldHint
249+
}
250+
251+
// ContentTrustForTool returns the content trust level based on tool annotations.
252+
// Open-world tools return "untrusted", closed-world tools return "trusted".
253+
func ContentTrustForTool(annotations *config.ToolAnnotations) string {
254+
if IsOpenWorldTool(annotations) {
255+
return ContentTrustUntrusted
256+
}
257+
return ContentTrustTrusted
258+
}
259+
230260
// isValidDataSensitivity checks if the data sensitivity is valid
231261
func isValidDataSensitivity(sensitivity string) bool {
232262
for _, valid := range ValidDataSensitivities {

internal/contracts/intent_test.go

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,112 @@ import (
66
"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
77
)
88

9+
func TestIsOpenWorldTool(t *testing.T) {
10+
trueVal := true
11+
falseVal := false
12+
13+
tests := []struct {
14+
name string
15+
annotations *config.ToolAnnotations
16+
want bool
17+
}{
18+
{
19+
name: "nil annotations - defaults to true (open world)",
20+
annotations: nil,
21+
want: true,
22+
},
23+
{
24+
name: "empty annotations (no hints set) - defaults to true",
25+
annotations: &config.ToolAnnotations{},
26+
want: true,
27+
},
28+
{
29+
name: "openWorldHint nil - defaults to true",
30+
annotations: &config.ToolAnnotations{
31+
ReadOnlyHint: &trueVal, // other hint set but not openWorldHint
32+
},
33+
want: true,
34+
},
35+
{
36+
name: "openWorldHint explicitly true",
37+
annotations: &config.ToolAnnotations{
38+
OpenWorldHint: &trueVal,
39+
},
40+
want: true,
41+
},
42+
{
43+
name: "openWorldHint explicitly false",
44+
annotations: &config.ToolAnnotations{
45+
OpenWorldHint: &falseVal,
46+
},
47+
want: false,
48+
},
49+
{
50+
name: "openWorldHint false with other hints",
51+
annotations: &config.ToolAnnotations{
52+
OpenWorldHint: &falseVal,
53+
ReadOnlyHint: &trueVal,
54+
DestructiveHint: &falseVal,
55+
},
56+
want: false,
57+
},
58+
}
59+
60+
for _, tt := range tests {
61+
t.Run(tt.name, func(t *testing.T) {
62+
got := IsOpenWorldTool(tt.annotations)
63+
if got != tt.want {
64+
t.Errorf("IsOpenWorldTool() = %v, want %v", got, tt.want)
65+
}
66+
})
67+
}
68+
}
69+
70+
func TestContentTrustForTool(t *testing.T) {
71+
trueVal := true
72+
falseVal := false
73+
74+
tests := []struct {
75+
name string
76+
annotations *config.ToolAnnotations
77+
want string
78+
}{
79+
{
80+
name: "nil annotations - untrusted",
81+
annotations: nil,
82+
want: ContentTrustUntrusted,
83+
},
84+
{
85+
name: "empty annotations - untrusted",
86+
annotations: &config.ToolAnnotations{},
87+
want: ContentTrustUntrusted,
88+
},
89+
{
90+
name: "openWorldHint true - untrusted",
91+
annotations: &config.ToolAnnotations{
92+
OpenWorldHint: &trueVal,
93+
},
94+
want: ContentTrustUntrusted,
95+
},
96+
{
97+
name: "openWorldHint false - trusted",
98+
annotations: &config.ToolAnnotations{
99+
OpenWorldHint: &falseVal,
100+
},
101+
want: ContentTrustTrusted,
102+
},
103+
}
104+
105+
for _, tt := range tests {
106+
t.Run(tt.name, func(t *testing.T) {
107+
got := ContentTrustForTool(tt.annotations)
108+
if got != tt.want {
109+
t.Errorf("ContentTrustForTool() = %v, want %v", got, tt.want)
110+
}
111+
})
112+
}
113+
}
114+
9115
func TestIntentDeclaration_Validate(t *testing.T) {
10116
// Note: Validate() only checks optional fields (data_sensitivity, reason)
11117
// operation_type is inferred from tool variant, not validated here
@@ -99,12 +205,12 @@ func TestIntentDeclaration_ValidateForToolVariant(t *testing.T) {
99205
// Note: ValidateForToolVariant now SETS operation_type from tool variant (inference)
100206
// It no longer validates that operation_type matches - it always overwrites with inferred value
101207
tests := []struct {
102-
name string
103-
intent IntentDeclaration
104-
toolVariant string
105-
wantErr bool
106-
wantErrCode string
107-
wantOpType string // expected operation_type after call
208+
name string
209+
intent IntentDeclaration
210+
toolVariant string
211+
wantErr bool
212+
wantErrCode string
213+
wantOpType string // expected operation_type after call
108214
}{
109215
{
110216
name: "empty intent with call_tool_read - sets operation_type",

0 commit comments

Comments
 (0)