Add support for standard MCP tool annotations
Overview
Add support for standard MCP tool annotations as defined in the Model Context Protocol specification.
Currently, the annotations
field in generated MCP tools is always set to None
. This feature will automatically populate appropriate annotation hints based on the HTTP verb extracted from OpenAPI operation definitions.
HTTP Verb to MCP Annotation Mapping
The following table defines the semantic mapping from HTTP verbs to MCP annotation hints:
HTTP Verb | readOnlyHint |
destructiveHint |
idempotentHint |
openWorldHint |
Rationale |
---|---|---|---|---|---|
GET | true |
false |
true |
true |
Safe, idempotent read operation per HTTP semantics |
HEAD | true |
false |
true |
true |
Safe, idempotent metadata retrieval (like GET without body) |
OPTIONS | true |
false |
true |
true |
Safe, idempotent capability discovery |
POST | false |
false |
false |
true |
Creates resources; not idempotent, not destructive to existing data |
PUT | false |
true |
true |
true |
Replaces/updates resources; idempotent but destructive |
PATCH | false |
true |
false |
true |
Modifies resources; destructive and typically not idempotent |
DELETE | false |
true |
true |
true |
Removes resources; idempotent (subsequent deletes are no-ops) but destructive |
Notes on Mapping
-
readOnlyHint
: OnlyGET
,HEAD
, andOPTIONS
are considered read-only per HTTP specifications -
destructiveHint
:PUT
,PATCH
, andDELETE
modify or remove existing resources -
idempotentHint
:GET
,HEAD
,OPTIONS
,PUT
, andDELETE
are idempotent per HTTP specs;POST
andPATCH
are not -
openWorldHint
: Alwaystrue
since OpenAPI tools interact with external HTTP APIs -
title
: Should continue to be populated from OpenAPI operationsummary
or generated from operation ID
Implementation Plan
Phase 1: Add Annotation Generation Logic
File: crates/rmcp-openapi/src/tool/metadata.rs
-
Import ToolAnnotations:
use rmcp::model::ToolAnnotations;
-
Add method to ToolMetadata:
impl ToolMetadata { /// Generate MCP annotations based on HTTP method semantics pub fn generate_annotations(&self) -> Option<ToolAnnotations> { match self.method.to_uppercase().as_str() { "GET" | "HEAD" | "OPTIONS" => Some(ToolAnnotations { title: None, // handled separately via metadata.title read_only_hint: Some(true), destructive_hint: Some(false), idempotent_hint: Some(true), open_world_hint: Some(true), }), "POST" => Some(ToolAnnotations { title: None, read_only_hint: Some(false), destructive_hint: Some(false), idempotent_hint: Some(false), open_world_hint: Some(true), }), "PUT" => Some(ToolAnnotations { title: None, read_only_hint: Some(false), destructive_hint: Some(true), idempotent_hint: Some(true), open_world_hint: Some(true), }), "PATCH" => Some(ToolAnnotations { title: None, read_only_hint: Some(false), destructive_hint: Some(true), idempotent_hint: Some(false), open_world_hint: Some(true), }), "DELETE" => Some(ToolAnnotations { title: None, read_only_hint: Some(false), destructive_hint: Some(true), idempotent_hint: Some(true), open_world_hint: Some(true), }), _ => None, // Unknown/unsupported HTTP methods } } }
-
Update
From<&ToolMetadata> for Tool
implementation:impl From<&ToolMetadata> for Tool { fn from(metadata: &ToolMetadata) -> Self { // ... existing code ... Tool { name: metadata.name.clone().into(), description: metadata.description.clone().map(|d| d.into()), input_schema, output_schema, annotations: metadata.generate_annotations(), // ← CHANGED: was None title: metadata.title.clone(), icons: None, } } }
Phase 2: Add Tests
File: crates/rmcp-openapi/src/tool/metadata.rs
(test module)
Add comprehensive unit tests:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_annotations() {
let metadata = ToolMetadata {
name: "test".into(),
title: None,
description: None,
parameters: serde_json::json!({}),
output_schema: None,
method: "GET".into(),
path: "/test".into(),
security: None,
};
let annotations = metadata.generate_annotations().unwrap();
assert_eq!(annotations.read_only_hint, Some(true));
assert_eq!(annotations.destructive_hint, Some(false));
assert_eq!(annotations.idempotent_hint, Some(true));
assert_eq!(annotations.open_world_hint, Some(true));
}
#[test]
fn test_post_annotations() {
// ... test POST mapping
}
#[test]
fn test_put_annotations() {
// ... test PUT mapping
}
#[test]
fn test_patch_annotations() {
// ... test PATCH mapping
}
#[test]
fn test_delete_annotations() {
// ... test DELETE mapping
}
#[test]
fn test_unknown_method_annotations() {
let metadata = ToolMetadata {
method: "UNKNOWN".into(),
// ... other fields
};
assert_eq!(metadata.generate_annotations(), None);
}
}
Phase 3: Integration Testing
Add snapshot tests or integration tests that verify:
- Tools generated from OpenAPI specs include correct annotations
- Annotations are properly serialized in MCP protocol responses
- Existing behavior remains unchanged (backward compatibility)
Phase 4: Documentation
- Update module documentation in
tool/metadata.rs
to explain annotation generation - Add example in README or documentation showing how annotations are automatically set
- Update CHANGELOG.md
Acceptance Criteria
Functional Requirements
-
AC1: Tools generated from GET
operations havereadOnlyHint: true
,destructiveHint: false
,idempotentHint: true
,openWorldHint: true
-
AC2: Tools generated from POST
operations havereadOnlyHint: false
,destructiveHint: false
,idempotentHint: false
,openWorldHint: true
-
AC3: Tools generated from PUT
operations havereadOnlyHint: false
,destructiveHint: true
,idempotentHint: true
,openWorldHint: true
-
AC4: Tools generated from PATCH
operations havereadOnlyHint: false
,destructiveHint: true
,idempotentHint: false
,openWorldHint: true
-
AC5: Tools generated from DELETE
operations havereadOnlyHint: false
,destructiveHint: true
,idempotentHint: true
,openWorldHint: true
-
AC6: Tools generated from HEAD
andOPTIONS
operations have same annotations asGET
-
AC7: Tools generated from unknown/unsupported HTTP methods have annotations: None
-
AC8: The title
field in annotations remainsNone
(title is handled separately viaTool.title
)
Technical Requirements
-
AC9: HTTP method comparison is case-insensitive -
AC10: All annotation fields are explicitly set (no reliance on defaults) -
AC11: Changes are isolated to tool/metadata.rs
(no changes to tool generation logic) -
AC12: Existing tools without annotations continue to work (backward compatibility)
Testing Requirements
-
AC13: Unit tests cover all HTTP verbs (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) -
AC14: Unit test covers unknown HTTP method edge case -
AC15: Integration test verifies annotations appear in generated tools from OpenAPI spec -
AC16: Snapshot tests (if present) are updated to reflect new annotations
Documentation Requirements
-
AC17: Code includes inline documentation explaining the mapping rationale -
AC18: Public API documentation is updated (if applicable)
References
- MCP Tool Annotations Specification
- HTTP Method Semantics (RFC 9110)
- Current implementation:
crates/rmcp-openapi/src/tool/metadata.rs:69
(annotations field set to None)
Edited by Jean-Marc Le Roux