Skip to content

Commit eaa3aa0

Browse files
zhuqingyvzhuqingyu-netizenclaude
authored
fix(team-mcp): use fixed server name to stay within 64-char tool limit (ELECTRON-1JY) (#336)
## Summary - Anthropic caps tool names at 64 chars (wire form `mcp__<server>__<tool>`); embedding a 36-char UUID v7 `team_id` into the team MCP server name pushed `team_describe_assistant` to 78 chars and triggered `invalid_request_error: 工具名称过长` (Sentry ELECTRON-1JY). - Hoist the server name to a fixed `TEAM_MCP_SERVER_NAME = "aionui-team"` constant in `aionui-api-types` (foundation layer reachable from both `aionui-team` and `aionui-ai-agent`); team routing was always done via per-team TCP port + auth token, so the team_id was redundant in the name. - Tighten the auto-approve prefix in `permission_router` from `mcp__aionui-team-` to `mcp__aionui-team__` (precise double-underscore — the old single-dash form happened to also catch `mcp__aionui-team-guide__`, which is now matched by its own dedicated entry). - Pin the limit with a unit test that asserts the longest current tool name (`team_describe_assistant`) plus `mcp__aionui-team__` stays ≤ 64 chars, so any future tool addition that would re-break the limit fails locally. ## Test plan - [x] `cargo fmt --all -- --check` - [x] `cargo clippy -p aionui-api-types -p aionui-team -p aionui-ai-agent --all-targets -- -D warnings` - [x] `cargo test -p aionui-api-types -p aionui-team -p aionui-ai-agent` - [x] `just push` full-workspace nextest gate (5657 tests passed) - [x] `just build` release binary built and reinstalled to `~/.cargo/bin/aioncore`; running AionUi Dev resolves to the new binary via system PATH 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: zhuqingyu <zhuqingyu@bituniverse.org> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d65c8ed commit eaa3aa0

9 files changed

Lines changed: 65 additions & 26 deletions

File tree

crates/aionui-ai-agent/src/factory/acp_assembler.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::capability::team_guide_prompt;
22
use crate::shared_kernel::PersistedSessionState;
33
use agent_client_protocol::schema::{EnvVariable, McpServer, McpServerStdio, NewSessionRequest};
44
use aionui_api_types::AgentMetadata;
5-
use aionui_api_types::{AcpBuildExtra, GuideMcpConfig, TeamMcpStdioConfig};
5+
use aionui_api_types::{AcpBuildExtra, GuideMcpConfig, TEAM_MCP_SERVER_NAME, TeamMcpStdioConfig};
66
use aionui_common::CommandSpec;
77
use std::path::PathBuf;
88

@@ -148,7 +148,7 @@ fn team_mcp_server(cfg: &TeamMcpStdioConfig) -> McpServer {
148148
EnvVariable::new(TeamMcpStdioConfig::ENV_TOKEN.to_owned(), cfg.token.clone()),
149149
EnvVariable::new(TeamMcpStdioConfig::ENV_SLOT_ID.to_owned(), cfg.slot_id.clone()),
150150
];
151-
let stdio = McpServerStdio::new(format!("aionui-team-{}", cfg.team_id), &cfg.binary_path)
151+
let stdio = McpServerStdio::new(TEAM_MCP_SERVER_NAME, &cfg.binary_path)
152152
.args(vec!["mcp-team-stdio".to_owned()])
153153
.env(env);
154154
McpServer::Stdio(stdio)
@@ -238,7 +238,7 @@ mod tests {
238238
let servers = resolve_mcp_servers(&config, "conv-1", Vec::new());
239239
assert_eq!(servers.len(), 1);
240240
match &servers[0] {
241-
McpServer::Stdio(s) => assert!(s.name.contains("team-1")),
241+
McpServer::Stdio(s) => assert_eq!(s.name, TEAM_MCP_SERVER_NAME),
242242
_ => panic!("expected stdio server"),
243243
}
244244
}
@@ -365,7 +365,7 @@ mod tests {
365365
let servers = resolve_mcp_servers(&config, "conv-1", user);
366366
assert_eq!(servers.len(), 2);
367367
match &servers[0] {
368-
McpServer::Stdio(s) => assert!(s.name.contains("team-1"), "team must come first"),
368+
McpServer::Stdio(s) => assert_eq!(s.name, TEAM_MCP_SERVER_NAME, "team must come first"),
369369
_ => panic!("expected stdio"),
370370
}
371371
match &servers[1] {

crates/aionui-ai-agent/src/factory/aionrs.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::sync::Arc;
33

44
use aion_agent::session::SessionManager;
55
use aion_config::config::{McpServerConfig, TransportType};
6-
use aionui_api_types::{AionrsBuildExtra, GuideMcpConfig, TeamMcpStdioConfig};
6+
use aionui_api_types::{AionrsBuildExtra, GuideMcpConfig, TEAM_MCP_SERVER_NAME, TeamMcpStdioConfig};
77
use aionui_common::AppError;
88
use tracing::{debug, info};
99

@@ -291,7 +291,7 @@ fn team_mcp_to_config(cfg: &TeamMcpStdioConfig) -> HashMap<String, McpServerConf
291291
deferred: Some(false),
292292
};
293293

294-
HashMap::from([(format!("aionui-team-{}", cfg.team_id), server)])
294+
HashMap::from([(TEAM_MCP_SERVER_NAME.to_owned(), server)])
295295
}
296296

297297
fn guide_mcp_to_config(
@@ -496,9 +496,9 @@ mod tests {
496496

497497
let result = resolve_mcp_servers(&overrides, "conv-1");
498498
assert_eq!(result.len(), 1);
499-
assert!(result.contains_key("aionui-team-team-42"));
499+
assert!(result.contains_key(TEAM_MCP_SERVER_NAME));
500500

501-
let server = &result["aionui-team-team-42"];
501+
let server = &result[TEAM_MCP_SERVER_NAME];
502502
assert_eq!(server.transport, TransportType::Stdio);
503503
assert_eq!(server.command.as_deref(), Some("/usr/bin/backend"));
504504
assert_eq!(server.args.as_deref(), Some(&["mcp-team-stdio".to_owned()][..]));

crates/aionui-ai-agent/src/manager/acp/permission_router.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ use tokio::sync::{Mutex, mpsc, oneshot};
99
use tracing::debug;
1010

1111
/// MCP tool prefixes that are auto-approved without user permission.
12-
const AUTO_APPROVE_PREFIXES: &[&str] = &["mcp__aionui-team-", "mcp__aionui-team-guide__"];
12+
///
13+
/// `mcp__aionui-team__` matches the team stdio bridge tools (server name is now
14+
/// the fixed `aionui-team` — see `TEAM_MCP_SERVER_NAME`).
15+
/// `mcp__aionui-team-guide__` matches the solo-session Guide tools.
16+
const AUTO_APPROVE_PREFIXES: &[&str] = &["mcp__aionui-team__", "mcp__aionui-team-guide__"];
1317

1418
/// Routes ACP permission requests from the protocol layer to the user
1519
/// (via `event_tx`) and back (via `confirm`). Owns the receiver channel

crates/aionui-api-types/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,5 +131,5 @@ pub use team::{
131131
TeamAgentShutdownPayload, TeamAgentSpawnedPayload, TeamAgentStatusPayload, TeamListResponse, TeamMcpPhase,
132132
TeamMcpStatusPayload, TeamResponse, TeammateMessagePayload,
133133
};
134-
pub use team_mcp::{GuideMcpConfig, TeamMcpStdioConfig};
134+
pub use team_mcp::{GuideMcpConfig, TEAM_MCP_SERVER_NAME, TeamMcpStdioConfig};
135135
pub use websocket::WebSocketMessage;

crates/aionui-api-types/src/team_mcp.rs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@
66
77
use serde::{Deserialize, Serialize};
88

9+
/// Fixed wire-level MCP server name for the team stdio bridge.
10+
///
11+
/// Anthropic's tool name regex caps total length at 64 chars and the wire-level
12+
/// tool name is `mcp__<server_name>__<tool>`. A 36-char UUID v7 `team_id`
13+
/// embedded in the server name pushed `team_describe_assistant` to 78 chars and
14+
/// caused `invalid_request_error: 工具名称过长` (ELECTRON-1JY). Team routing
15+
/// has always been done via per-team TCP port + auth token, so the team_id was
16+
/// redundant in the server name.
17+
pub const TEAM_MCP_SERVER_NAME: &str = "aionui-team";
18+
919
/// Connection config for the Guide MCP stdio server in solo conversations.
1020
///
1121
/// Passed through `AcpBuildExtra::guide_mcp_config` by the factory so that
@@ -20,10 +30,9 @@ pub struct GuideMcpConfig {
2030

2131
/// Stdio connection config for the team session MCP server.
2232
///
23-
/// `team_id` is persisted alongside the connection triple so every
24-
/// consumer (D3 spec builder, D10 ACP injector, D7 bridge subcommand)
25-
/// can derive the wire-level MCP server name `aionui-team-<team_id>`
26-
/// without threading a second parameter through unrelated call sites.
33+
/// `team_id` is persisted for diagnostics; the wire-level MCP server name is
34+
/// the fixed [`TEAM_MCP_SERVER_NAME`] (team routing happens via per-team TCP
35+
/// port + auth token, not via the server name — see ELECTRON-1JY).
2736
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2837
pub struct TeamMcpStdioConfig {
2938
pub team_id: String,
@@ -60,6 +69,29 @@ mod tests {
6069
assert_eq!(cfg, parsed);
6170
}
6271

72+
/// ELECTRON-1JY regression: Anthropic caps tool names at 64 chars,
73+
/// where the wire-level name is `mcp__<server_name>__<tool>`. The
74+
/// previous design embedded a 36-char UUID v7 `team_id` into the
75+
/// server name, which pushed `team_describe_assistant` to 78 chars
76+
/// and triggered `invalid_request_error: 工具名称过长`.
77+
///
78+
/// This test pins the longest known team tool name against the
79+
/// 64-char bound so any future tool / rename that would re-break the
80+
/// limit fails locally instead of in production.
81+
#[test]
82+
fn team_mcp_tool_names_stay_within_anthropic_64_char_limit() {
83+
// Longest tool name currently registered on the team MCP server.
84+
// Update if a longer-named tool is added.
85+
let longest_tool = "team_describe_assistant";
86+
let wire_name = format!("mcp__{TEAM_MCP_SERVER_NAME}__{longest_tool}");
87+
assert!(
88+
wire_name.len() <= 64,
89+
"Anthropic 64-char tool-name limit exceeded: {} ({} chars)",
90+
wire_name,
91+
wire_name.len()
92+
);
93+
}
94+
6395
#[test]
6496
fn deserialization_tolerates_unknown_fields() {
6597
// Forward-compat: extra fields in persisted `conversation.extra.team_mcp_stdio_config`

crates/aionui-team/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ pub use error::TeamError;
2121
pub use events::TeamEventEmitter;
2222
pub use guide::{GuideMcpServer, handle_aion_list_models};
2323
pub use mailbox::Mailbox;
24-
pub use mcp::{TeamMcpServer, TeamMcpStdioConfig, TeamMcpStdioServerSpec};
24+
pub use mcp::{TEAM_MCP_SERVER_NAME, TeamMcpServer, TeamMcpStdioConfig, TeamMcpStdioServerSpec};
25+
2526
pub use prompts::{build_lead_prompt, build_teammate_prompt, build_wake_payload};
2627
pub use routes::{TeamRouterState, team_routes};
2728
pub use scheduler::{

crates/aionui-team/src/mcp/bridge.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ use std::path::PathBuf;
1212

1313
use agent_client_protocol::schema::{EnvVariable, McpServer, McpServerStdio};
1414

15-
pub use aionui_api_types::TeamMcpStdioConfig;
15+
pub use aionui_api_types::{TEAM_MCP_SERVER_NAME, TeamMcpStdioConfig};
1616

1717
/// Stdio MCP server description ready to be handed to `session/new`.
1818
///
19-
/// Field shapes are fixed by [phase1 interface-contracts.md §3]:
20-
/// - `name` = `"aionui-team-<team_id>"`
19+
/// Field shapes:
20+
/// - `name` = `"aionui-team"` (fixed; team routing is done via `port` + `token`,
21+
/// not via the server name — see `TeamMcpServer` per-team TCP listener)
2122
/// - `command` = absolute path to the backend binary (resolved via
2223
/// `std::env::current_exe()` at app startup)
2324
/// - `args` = `["mcp-bridge"]`
@@ -35,12 +36,12 @@ impl TeamMcpStdioServerSpec {
3536
///
3637
/// `backend_binary_path` is the absolute path to the `aioncore`
3738
/// executable (phase1 single-binary constraint — no standalone bridge).
38-
/// `team_id` is read from `cfg.team_id` rather than taken as a separate
39-
/// parameter so the wire-level server name stays in sync with the
40-
/// persisted config across every consumer.
4139
pub fn from_config(backend_binary_path: &str, cfg: &TeamMcpStdioConfig) -> Self {
40+
// `cfg.team_id` is intentionally not embedded in the server name — see
41+
// `TEAM_MCP_SERVER_NAME` doc comment. It is still kept on the persisted
42+
// config for diagnostics and future consumers.
4243
Self {
43-
name: format!("aionui-team-{}", cfg.team_id),
44+
name: TEAM_MCP_SERVER_NAME.to_owned(),
4445
command: backend_binary_path.to_owned(),
4546
args: vec!["mcp-bridge".to_owned()],
4647
env: vec![
@@ -87,7 +88,7 @@ mod tests {
8788
fn from_config_fills_all_fields() {
8889
let spec = TeamMcpStdioServerSpec::from_config("/usr/bin/aioncore", &sample_cfg());
8990

90-
assert_eq!(spec.name, "aionui-team-team-42");
91+
assert_eq!(spec.name, TEAM_MCP_SERVER_NAME);
9192
assert_eq!(spec.command, "/usr/bin/aioncore");
9293
assert_eq!(spec.args, vec!["mcp-bridge".to_owned()]);
9394
assert_eq!(spec.env.len(), 3);
@@ -118,7 +119,7 @@ mod tests {
118119

119120
// `Stdio` variant is `#[serde(untagged)]` inside `McpServer`, so the
120121
// JSON is the raw `McpServerStdio` shape — no `"type":"stdio"` tag.
121-
assert_eq!(json["name"], "aionui-team-team-42");
122+
assert_eq!(json["name"], TEAM_MCP_SERVER_NAME);
122123
assert_eq!(json["command"], "/bin/aioncore");
123124
assert_eq!(json["args"], serde_json::json!(["mcp-bridge"]));
124125

crates/aionui-team/src/mcp/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ pub mod protocol;
33
pub mod server;
44
pub mod tools;
55

6+
pub use aionui_api_types::TEAM_MCP_SERVER_NAME;
67
pub use bridge::{TeamMcpStdioConfig, TeamMcpStdioServerSpec};
78
pub use server::TeamMcpServer;

crates/aionui-team/src/session.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,10 +1124,10 @@ mod tests {
11241124
}
11251125

11261126
#[tokio::test]
1127-
async fn stdio_spec_embeds_team_and_binary_path() {
1127+
async fn stdio_spec_uses_fixed_name_and_binary_path() {
11281128
let session = start_session().await;
11291129
let spec = session.stdio_spec("lead-1");
1130-
assert_eq!(spec.name, "aionui-team-t1");
1130+
assert_eq!(spec.name, crate::mcp::TEAM_MCP_SERVER_NAME);
11311131
assert_eq!(spec.command, "/tmp/aioncore-test");
11321132
assert_eq!(spec.args, vec!["mcp-bridge".to_string()]);
11331133
assert!(spec.env.iter().any(|(k, v)| k == "TEAM_AGENT_SLOT_ID" && v == "lead-1"));

0 commit comments

Comments
 (0)