|
16 | 16 |
|
17 | 17 | use serde_json::Value; |
18 | 18 |
|
19 | | -use crate::model::{ExtensionCapabilities, JsonObject, Tool}; |
| 19 | +use crate::model::{ExtensionCapabilities, JsonObject, Meta, Tool}; |
20 | 20 |
|
21 | 21 | /// `_meta` key Claude Code honours on a tool's `tools/list` entry to raise the |
22 | 22 | /// per-tool text-output size threshold past the default 25 000-token cap. |
@@ -59,6 +59,38 @@ impl Tool { |
59 | 59 | } |
60 | 60 | } |
61 | 61 |
|
| 62 | +/// Build a [`Meta`] carrying the `anthropic/maxResultSizeChars` key for direct |
| 63 | +/// use in the `#[tool(meta = ...)]` macro attribute. The value is clamped to |
| 64 | +/// [`MAX_RESULT_SIZE_CHARS_CEILING`]. |
| 65 | +/// |
| 66 | +/// Prefer this helper in macro-driven tool routers where you cannot call |
| 67 | +/// [`Tool::with_anthropic_max_result_size_chars`] at construction time. The |
| 68 | +/// `_meta` key is silently ignored by every MCP client other than Claude Code. |
| 69 | +/// |
| 70 | +/// See <https://docs.anthropic.com/en/docs/claude-code/mcp#raise-the-limit-for-a-specific-tool>. |
| 71 | +/// |
| 72 | +/// # Example |
| 73 | +/// |
| 74 | +/// ```ignore |
| 75 | +/// use rmcp::anthropic_ext::anthropic_max_result_size_chars_meta; |
| 76 | +/// |
| 77 | +/// #[tool( |
| 78 | +/// annotations(read_only_hint = true), |
| 79 | +/// meta = anthropic_max_result_size_chars_meta(500_000), |
| 80 | +/// )] |
| 81 | +/// async fn big_output(&self) -> Result<Json<Resp>, String> { /* ... */ } |
| 82 | +/// ``` |
| 83 | +#[must_use] |
| 84 | +pub fn anthropic_max_result_size_chars_meta(chars: u32) -> Meta { |
| 85 | + let clamped = chars.min(MAX_RESULT_SIZE_CHARS_CEILING); |
| 86 | + let mut meta = Meta::new(); |
| 87 | + meta.0.insert( |
| 88 | + MAX_RESULT_SIZE_CHARS_META_KEY.to_string(), |
| 89 | + Value::from(clamped), |
| 90 | + ); |
| 91 | + meta |
| 92 | +} |
| 93 | + |
62 | 94 | /// Insert the `claude/channel` extension capability into an |
63 | 95 | /// [`ExtensionCapabilities`] map with empty settings. Pass the resulting map |
64 | 96 | /// to [`ServerCapabilitiesBuilder::enable_extensions_with`] when building |
@@ -146,6 +178,34 @@ mod tests { |
146 | 178 | assert!(stored.is_empty(), "expected empty settings object"); |
147 | 179 | } |
148 | 180 |
|
| 181 | + #[test] |
| 182 | + fn anthropic_max_result_size_chars_meta_under_ceiling() { |
| 183 | + let meta = anthropic_max_result_size_chars_meta(100_000); |
| 184 | + let stored = meta |
| 185 | + .0 |
| 186 | + .get(MAX_RESULT_SIZE_CHARS_META_KEY) |
| 187 | + .cloned() |
| 188 | + .expect("key present"); |
| 189 | + assert_eq!(stored, Value::from(100_000u32)); |
| 190 | + } |
| 191 | + |
| 192 | + #[test] |
| 193 | + fn anthropic_max_result_size_chars_meta_clamps_above_ceiling() { |
| 194 | + let meta = anthropic_max_result_size_chars_meta(1_000_000); |
| 195 | + let stored = meta |
| 196 | + .0 |
| 197 | + .get(MAX_RESULT_SIZE_CHARS_META_KEY) |
| 198 | + .cloned() |
| 199 | + .expect("key present"); |
| 200 | + assert_eq!(stored, Value::from(MAX_RESULT_SIZE_CHARS_CEILING)); |
| 201 | + } |
| 202 | + |
| 203 | + #[test] |
| 204 | + fn anthropic_max_result_size_chars_meta_is_standalone() { |
| 205 | + let meta = anthropic_max_result_size_chars_meta(100_000); |
| 206 | + assert_eq!(meta.0.len(), 1, "helper should only emit the size key"); |
| 207 | + } |
| 208 | + |
149 | 209 | #[test] |
150 | 210 | fn warn_if_over_2kb_returns_true_above_limit() { |
151 | 211 | let body = "x".repeat(lint::SIZE_LIMIT_BYTES + 1); |
|
0 commit comments