Skip to content

Commit f9b42c9

Browse files
committed
feat(anthropic-ext): add Claude Code-specific server extensions
Off-by-default `anthropic-ext` feature exposing the three server-facing extensions Claude Code implements beyond the upstream MCP spec: - `Tool::with_anthropic_max_result_size_chars(u32)` sets the `anthropic/maxResultSizeChars` `_meta` key (clamped to 500 000) to raise the per-tool text-output threshold past Claude Code's default 25 000-token cap. - `insert_claude_channel(&mut ExtensionCapabilities)` registers the experimental `claude/channel` capability for `--channels` push messages. - `anthropic_ext::lint::warn_if_over_2kb(...)` tracing helper for catching Claude Code's 2 KB description / instructions truncation cliff. All members are passive: silently ignored by every MCP client other than Claude Code. Single new module, one new feature flag, no changes to existing rmcp code so upstream merge conflicts stay localized. Refs: docs.anthropic.com/en/docs/claude-code/mcp
1 parent 63583b1 commit f9b42c9

3 files changed

Lines changed: 169 additions & 0 deletions

File tree

crates/rmcp/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ exhaustive_enums = "warn"
1515

1616
[package.metadata.docs.rs]
1717
features = [
18+
"anthropic-ext",
1819
"auth",
1920
"auth-client-credentials-jwt",
2021
"base64",
@@ -119,6 +120,10 @@ client = ["dep:tokio-stream"]
119120
server = ["transport-async-rw", "dep:schemars", "dep:pastey"]
120121
macros = ["dep:rmcp-macros", "dep:pastey"]
121122
elicitation = ["dep:url"]
123+
# Claude Code / claude-cli specific server extensions: anthropic/maxResultSizeChars,
124+
# claude/channel capability, 2KB truncation lint. Off by default; opt in when
125+
# the fleet targets Claude Code as the MCP client.
126+
anthropic-ext = []
122127

123128
# reqwest http client
124129
__reqwest = ["dep:reqwest"]

crates/rmcp/src/anthropic_ext.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
//! Claude Code-specific MCP extensions.
2+
//!
3+
//! Claude Code (claude-cli) implements two server-facing extensions that aren't in
4+
//! the upstream MCP spec: a per-tool output-size override carried in `_meta`
5+
//! (`anthropic/maxResultSizeChars`, raising the default 25k-token cap up to a
6+
//! 500 000-char ceiling), and an experimental push-message capability
7+
//! (`claude/channel`). This module exposes constants plus a [`Tool`] builder
8+
//! extension so servers can opt into both without hand-rolling JSON keys.
9+
//!
10+
//! All members are passive: they emit what Claude Code expects and are
11+
//! silently ignored by every other MCP client.
12+
//!
13+
//! See <https://docs.anthropic.com/en/docs/claude-code/mcp>.
14+
//!
15+
//! Enable via the `anthropic-ext` cargo feature on `rmcp`.
16+
17+
use serde_json::Value;
18+
19+
use crate::model::{ExtensionCapabilities, JsonObject, Tool};
20+
21+
/// `_meta` key Claude Code honours on a tool's `tools/list` entry to raise the
22+
/// per-tool text-output size threshold past the default 25 000-token cap.
23+
///
24+
/// See <https://docs.anthropic.com/en/docs/claude-code/mcp#raise-the-limit-for-a-specific-tool>.
25+
pub const MAX_RESULT_SIZE_CHARS_META_KEY: &str = "anthropic/maxResultSizeChars";
26+
27+
/// Hard ceiling Claude Code applies to `anthropic/maxResultSizeChars` values.
28+
/// Tools requesting a larger threshold are clamped to this value.
29+
pub const MAX_RESULT_SIZE_CHARS_CEILING: u32 = 500_000;
30+
31+
/// Experimental capability key Claude Code looks for when started with
32+
/// `--channels`, allowing the server to push messages into the session
33+
/// as out-of-band notifications.
34+
///
35+
/// See <https://docs.anthropic.com/en/docs/claude-code/mcp#push-messages-with-channels>.
36+
pub const CLAUDE_CHANNEL_CAPABILITY: &str = "claude/channel";
37+
38+
impl Tool {
39+
/// Claude Code-specific: set the `anthropic/maxResultSizeChars` `_meta`
40+
/// entry on this tool, raising the per-tool text-output threshold in
41+
/// Claude Code up to a hard ceiling of
42+
/// [`MAX_RESULT_SIZE_CHARS_CEILING`] (500 000 chars).
43+
///
44+
/// Values above the ceiling are clamped; image output is not affected
45+
/// (only text `content` entries).
46+
///
47+
/// Silently ignored by every MCP client other than Claude Code.
48+
/// See <https://docs.anthropic.com/en/docs/claude-code/mcp#raise-the-limit-for-a-specific-tool>.
49+
#[must_use]
50+
pub fn with_anthropic_max_result_size_chars(mut self, chars: u32) -> Self {
51+
let clamped = chars.min(MAX_RESULT_SIZE_CHARS_CEILING);
52+
let mut meta = self.meta.unwrap_or_default();
53+
meta.0.insert(
54+
MAX_RESULT_SIZE_CHARS_META_KEY.to_string(),
55+
Value::from(clamped),
56+
);
57+
self.meta = Some(meta);
58+
self
59+
}
60+
}
61+
62+
/// Insert the `claude/channel` extension capability into an
63+
/// [`ExtensionCapabilities`] map with empty settings. Pass the resulting map
64+
/// to [`ServerCapabilitiesBuilder::enable_extensions_with`] when building
65+
/// your server's [`ServerCapabilities`].
66+
///
67+
/// Silently ignored by every MCP client other than Claude Code.
68+
///
69+
/// [`ServerCapabilitiesBuilder::enable_extensions_with`]: crate::model::ServerCapabilities
70+
/// [`ServerCapabilities`]: crate::model::ServerCapabilities
71+
pub fn insert_claude_channel(extensions: &mut ExtensionCapabilities) {
72+
extensions.insert(CLAUDE_CHANNEL_CAPABILITY.to_string(), JsonObject::new());
73+
}
74+
75+
/// Lint helpers for catching common Claude Code integration pitfalls at
76+
/// runtime. All helpers are pure - they `tracing::warn!` when a condition is
77+
/// violated but never change behavior.
78+
pub mod lint {
79+
/// Claude Code truncates tool descriptions and server `instructions` at
80+
/// 2 048 bytes each, silently and mid-sentence when under aggregate
81+
/// pressure across multiple servers. See
82+
/// <https://github.com/anthropics/claude-code/issues/43474>.
83+
pub const SIZE_LIMIT_BYTES: usize = 2 * 1024;
84+
85+
/// Emits a `tracing::warn!` when `body.len()` exceeds Claude Code's
86+
/// 2 KB truncation cliff. Returns `true` if a warning was emitted.
87+
///
88+
/// `label` should identify what was over the limit (e.g. the tool name or
89+
/// `"instructions.md"`).
90+
pub fn warn_if_over_2kb(label: &str, body: &str) -> bool {
91+
let size = body.len();
92+
if size > SIZE_LIMIT_BYTES {
93+
tracing::warn!(
94+
target: "rmcp::anthropic_ext::lint",
95+
label = label,
96+
size = size,
97+
limit = SIZE_LIMIT_BYTES,
98+
"Claude Code will silently truncate this content at the 2 KB cliff; front-load critical content (see claude-code#43474 for aggregate-truncation caveat)."
99+
);
100+
true
101+
} else {
102+
false
103+
}
104+
}
105+
}
106+
107+
#[cfg(test)]
108+
mod tests {
109+
use std::sync::Arc;
110+
111+
use super::*;
112+
113+
fn sample_tool() -> Tool {
114+
Tool::new("t", "d", Arc::new(JsonObject::new()))
115+
}
116+
117+
#[test]
118+
fn max_result_size_chars_is_clamped() {
119+
let tool = sample_tool().with_anthropic_max_result_size_chars(1_000_000);
120+
let meta = tool.meta.expect("meta set");
121+
let stored = meta
122+
.0
123+
.get(MAX_RESULT_SIZE_CHARS_META_KEY)
124+
.expect("key present");
125+
assert_eq!(stored, &Value::from(MAX_RESULT_SIZE_CHARS_CEILING));
126+
}
127+
128+
#[test]
129+
fn max_result_size_chars_under_ceiling_kept_verbatim() {
130+
let tool = sample_tool().with_anthropic_max_result_size_chars(100_000);
131+
let stored = tool
132+
.meta
133+
.expect("meta")
134+
.0
135+
.get(MAX_RESULT_SIZE_CHARS_META_KEY)
136+
.cloned()
137+
.expect("key");
138+
assert_eq!(stored, Value::from(100_000u32));
139+
}
140+
141+
#[test]
142+
fn claude_channel_extension_inserted_empty() {
143+
let mut ext = ExtensionCapabilities::new();
144+
insert_claude_channel(&mut ext);
145+
let stored = ext.get(CLAUDE_CHANNEL_CAPABILITY).expect("capability set");
146+
assert!(stored.is_empty(), "expected empty settings object");
147+
}
148+
149+
#[test]
150+
fn warn_if_over_2kb_returns_true_above_limit() {
151+
let body = "x".repeat(lint::SIZE_LIMIT_BYTES + 1);
152+
assert!(lint::warn_if_over_2kb("test", &body));
153+
}
154+
155+
#[test]
156+
fn warn_if_over_2kb_returns_false_at_limit() {
157+
let body = "x".repeat(lint::SIZE_LIMIT_BYTES);
158+
assert!(!lint::warn_if_over_2kb("test", &body));
159+
}
160+
}

crates/rmcp/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ mod error;
66
#[allow(deprecated)]
77
pub use error::{Error, ErrorData, RmcpError};
88

9+
/// Claude Code-specific server extensions (maxResultSizeChars, claude/channel).
10+
/// See <https://docs.anthropic.com/en/docs/claude-code/mcp>.
11+
#[cfg(feature = "anthropic-ext")]
12+
pub mod anthropic_ext;
913
/// Basic data types in MCP specification
1014
pub mod model;
1115
#[cfg(any(feature = "client", feature = "server"))]

0 commit comments

Comments
 (0)