AppServer V2方法注册机制
本文承接JSON-RPC封装与错误模型。App Server 的 V2 方法不是在每个 handler 中手写一套 request、response、wire method 和 schema;app-server-protocol/src/protocol/common.rs 使用声明宏,把方法定义展开为 typed enum、ID 访问器、method name、参数作用域、反向请求构造和 schema 导出辅助函数。
阅读这套机制时,要区分三种“注册”:宏调用中的方法表注册、运行时 ClientRequest/ServerRequest 枚举注册,以及 schema/experimental API 中的导出注册。它们共享一份声明,但作用时间不同。
1. Client方法表
源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: client_request_definitions!
macro_rules! client_request_definitions {
(
$(
$(#[experimental($reason:expr)])?
$(#[doc = $variant_doc:literal])*
$variant:ident => $wire:literal {
params: $(#[$params_meta:meta])* $params:ty,
$(inspect_params: $inspect_params:tt,)?
serialization: $serialization:ident $( ( $($serialization_args:tt)* ) )?,
$(manual_payload_conversion: $manual_payload_conversion:ident,)?
response: $response:ty,
}
),* $(,)?
) => {
/// Request from the client to the server.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "method", rename_all = "camelCase")]
pub enum ClientRequest {
$(
$(#[doc = $variant_doc])*
#[serde(rename = $wire)]
#[ts(rename = $wire)]
$variant {
#[serde(rename = "id")]
request_id: RequestId,
$(#[$params_meta])*
params: $params,
},
)*
}
};
}每个条目至少绑定 variant、wire method、params、serialization scope 和 response。inspect_params 允许一个总体稳定的方法根据参数中的实验字段继续判断;manual_payload_conversion 则为少数不能自动生成 payload conversion 的响应保留出口。
2. ClientRequest
源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: client_request_definitions! 展开的 ClientRequest
宏源码使用 $variant、$params 等 metavariable;真正编译出的 enum 会由每个条目替换这些变量。关键点是 wire method 作为 serde tag,request ID 作为字段,具体参数类型仍由 method 条目决定。
源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: ClientRequest::id、method_name
pub fn id(&self) -> &RequestId {
match self {
$(Self::$variant { request_id, .. } => request_id,)*
}
}
pub const fn method_name(&self) -> &'static str {
match self {
$(Self::$variant { .. } => $wire,)*
}
}任何新增方法都会自动获得 ID 和 wire name 访问器;handler 不需要自行维护字符串到 variant 的第二张表。
3. JSON到请求
源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: TryFrom<JSONRPCRequest> for ClientRequest
impl TryFrom<JSONRPCRequest> for ClientRequest {
type Error = serde_json::Error;
fn try_from(request: JSONRPCRequest) -> Result<Self, Self::Error> {
let JSONRPCRequest {
id: request_id,
method,
params,
trace: _,
} = request;
let mut request = serde_json::Map::new();
request.insert("id".to_string(), serde_json::to_value(request_id)?);
request.insert("method".to_string(), serde_json::Value::String(method));
if let Some(params) = params {
request.insert("params".to_string(), params);
}
serde_json::from_value(serde_json::Value::Object(request))
}
}这里把通用 envelope 重建成宏生成 enum 所需的 JSON 形状,再由 serde 根据 method 解码具体 params。trace 不进入 enum,是因为它已经在上层 RequestContext 中用于 tracing。
4. 参数作用域
源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: ClientRequestSerializationScope、serialization_scope_expr!
pub enum ClientRequestSerializationScope {
Global(&'static str),
GlobalSharedRead(&'static str),
Thread { thread_id: String },
ThreadPath { path: PathBuf },
CommandExecProcess { process_id: String },
Process { process_handle: String },
FuzzyFileSearchSession { session_id: String },
FsWatch { watch_id: String },
McpOauth { server_name: String },
}作用域不是权限判断,而是 App Server 决定请求应发送到哪些连接或由哪个共享状态处理的路由提示。宏把 thread_id(...)、thread_or_path(...)、process_handle(...) 等声明转换成统一的 enum,避免各 method handler 自己解释字段。
源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: serialization_scope_expr! 的 thread/process 分支
($actual_params:ident, thread_id($params:ident . $field:ident)) => {
Some(ClientRequestSerializationScope::Thread {
thread_id: $actual_params.$field.clone(),
})
};
($actual_params:ident, thread_or_path($params:ident . $thread_field:ident, $params2:ident . $path_field:ident)) => {
if !$actual_params.$thread_field.is_empty() {
Some(ClientRequestSerializationScope::Thread {
thread_id: $actual_params.$thread_field.clone(),
})
} else if let Some(path) = $actual_params.$path_field.clone() {
Some(ClientRequestSerializationScope::ThreadPath { path })
} else {
Some(ClientRequestSerializationScope::Thread {
thread_id: $actual_params.$thread_field.clone(),
})
}
};
($actual_params:ident, process_handle($params:ident . $field:ident)) => {
Some(ClientRequestSerializationScope::Process {
process_handle: $actual_params.$field.clone(),
})
};thread_or_path 的 fallback 保留空 thread ID,而不是静默返回 None;调用方可以据此发现请求既没有有效线程 ID 也没有 rollout path。
5. 实际方法声明
源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: client_request_definitions! 调用中的 thread methods
client_request_definitions! {
Initialize => "initialize" {
params: v1::InitializeParams,
serialization: None,
response: v1::InitializeResponse,
},
ThreadStart => "thread/start" {
params: v2::ThreadStartParams,
inspect_params: true,
serialization: None,
response: v2::ThreadStartResponse,
},
ThreadResume => "thread/resume" {
params: v2::ThreadResumeParams,
inspect_params: true,
serialization: thread_or_path(params.thread_id, params.path),
response: v2::ThreadResumeResponse,
},
ThreadArchive => "thread/archive" {
params: v2::ThreadArchiveParams,
serialization: thread_id(params.thread_id),
response: v2::ThreadArchiveResponse,
},
}这个表同时描述稳定方法和带参数实验字段的方法。ThreadResume 可能由 thread ID 或 rollout path 定位;ThreadArchive 只接受 thread ID。路由差异在声明层可见,运行时 helper 自动保持一致。
6. Response封装
源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: ClientResponse、ClientResponsePayload
pub enum ClientResponse {
$(
$(#[doc = $variant_doc])*
#[serde(rename = $wire)]
$variant {
#[serde(rename = "id")]
request_id: RequestId,
response: $response,
},
)*
}
impl ClientResponse {
pub fn id(&self) -> &RequestId {
match self {
$(Self::$variant { request_id, .. } => request_id,)*
}
}
pub fn into_jsonrpc_parts(
self,
) -> std::result::Result<(RequestId, crate::Result), serde_json::Error> {
match self {
$(Self::$variant { request_id, response } => {
serde_json::to_value(response).map(|result| (request_id, result))
}),*
}
}
}Response payload 也由同一份 method table 生成,ID 不会在 handler 返回 typed response 后丢失。ClientResponsePayload::to_jsonrpc_parts 将具体 response 序列化为通用 JSON result,供 transport 层构造 envelope。
7. Server反向请求
源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: server_request_definitions!
macro_rules! server_request_definitions {
(
$(
$(#[experimental($reason:expr)])?
$(#[doc = $variant_doc:literal])*
$variant:ident $(=> $wire:literal)? {
params: $params:ty,
response: $response:ty,
}
),* $(,)?
) => {
/// Request initiated from the server and sent to the client.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[allow(clippy::large_enum_variant)]
#[serde(tag = "method", rename_all = "camelCase")]
pub enum ServerRequest {
$(
$(#[doc = $variant_doc])*
$(#[serde(rename = $wire)] #[ts(rename = $wire)])?
$variant {
#[serde(rename = "id")]
request_id: RequestId,
params: $params,
},
)*
}
};
}服务器发给客户端的 request 使用另一张 method table,因为有些 client callback 没有对应的 incoming method。当前条目包含 command approval、file change approval、tool input、MCP elicitation、permissions request 等反向请求。
源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: ServerRequest::response_from_result、ServerRequestPayload::request_with_id
pub fn response_from_result(
&self,
result: crate::Result,
) -> serde_json::Result<ServerResponse> {
match self {
$(Self::$variant { request_id, .. } => {
let response = serde_json::from_value::<$response>(result)?;
Ok(ServerResponse::$variant {
request_id: request_id.clone(),
response,
})
}),*
}
}
pub fn request_with_id(self, request_id: RequestId) -> ServerRequest {
match self {
$(Self::$variant(params) => ServerRequest::$variant { request_id, params },)*
}
}反向请求先由 payload enum 携带 params,分配 ID 后变成 ServerRequest;客户端结果再按照原 request variant 的 response 类型解码。类型错误发生在 callback 完成前,不会被误当成业务成功。
8. 实验能力与schema
源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: experimental_reason_expr!、ExperimentalApi 实现
macro_rules! experimental_reason_expr {
(variant $variant:ident, #[experimental($reason:expr)] $params:ident $(, $inspect_params:tt)?) => {
Some($reason)
};
(variant $variant:ident, $params:ident, true) => {
crate::experimental_api::ExperimentalApi::experimental_reason($params)
};
(variant $variant:ident, $params:ident $(, $inspect_params:tt)?) => {
None
};
}方法级实验标记直接返回 reason;参数级检查递归调用参数类型的 ExperimentalApi。因此一个总体稳定的方法可以因为某个字段而被标记实验,客户端能力协商不必复制字段判断逻辑。
源码位置:codex-rs/app-server-protocol/src/experimental_api.rs :: ExperimentalApi、experimental_required_message
pub trait ExperimentalApi {
fn experimental_reason(&self) -> Option<&'static str>;
}
pub fn experimental_required_message(reason: &str) -> String {
format!("{reason} requires experimentalApi capability")
}
impl<T: ExperimentalApi> ExperimentalApi for Option<T> {
fn experimental_reason(&self) -> Option<&'static str> {
self.as_ref().and_then(ExperimentalApi::experimental_reason)
}
}
impl<T: ExperimentalApi> ExperimentalApi for Vec<T> {
fn experimental_reason(&self) -> Option<&'static str> {
self.iter().find_map(ExperimentalApi::experimental_reason)
}
}schema 导出函数会遍历宏生成的 request/response types,并按 stable/experimental API 选择输出集合。schema fixture 不是运行时 handler 测试,但它能发现 method、字段和生成文件之间的不一致。
9. 测试与边界
源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: jsonrpc_request_conversion_preserves_serde_enum_decoding、client_request_serialization_scope_covers_keyed_families
源码位置:codex-rs/app-server-protocol/src/protocol/common_tests.rs :: client_response_payload_serializes_without_an_intermediate_json_value
源码位置:codex-rs/app-server-protocol/src/schema_fixtures_tests.rs :: typescript_schema_fixtures_match_generated、json_schema_fixtures_match_generated、stable_precomputed_exports_match_schema_fixtures
cd codex-rs
cargo test -p codex-app-server-protocol jsonrpc_request_conversion_preserves_serde_enum_decoding -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol client_request_serialization_scope_covers_keyed_families -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol client_response_payload_serializes_without_an_intermediate_json_value -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol typescript_schema_fixtures_match_generated -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol json_schema_fixtures_match_generated -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol stable_precomputed_exports_match_schema_fixtures -- --nocapture --test-threads=1这些测试说明 typed request/response 转换、路由作用域和 schema 生成一致性;不能证明每个 method 的业务 handler 成功,也不能证明实验字段在所有客户端都被正确展示。
10. 源码定位练习
遇到“方法名能解析但参数错误”,先看 method table 中的 params type 和 ClientRequest::try_from;遇到“请求发到了错误连接”,检查 serialization scope,而不是修改 handler 内的 thread lookup。
遇到“客户端收到了未知实验字段”,检查 experimental_reason_expr!、initialize capability 和 schema fixture;遇到“反向请求无响应”,沿 ServerRequestPayload::request_with_id、pending callback、response_from_result 和 connection cleanup 逐层定位。
App Server 方法注册的价值在于消除平行协议表:wire method、typed params/response、ID 关联、路由作用域、实验能力和 schema 导出都从同一份声明生成。理解这份声明如何展开,比记住某个具体方法的 JSON 形状更能帮助你阅读后续协议代码。
