MCP与MemoryCitation类型
本文承接动态工具与执行记录和ConversationItem类型体系。MCP 与 memory citation 都会把“外部内容”带进一次 turn,但入口不同:MCP 从 server 能力、tool call 和 resource read 进入,MemoryCitation 则从 assistant 输出中的隐藏标记解析出来,再用于 memory usage accounting。
要读懂这篇,必须区分四种数据:server/tool/resource 的能力描述,MCP 调用的执行记录,resource 的来源授权,以及 assistant 文本中的 citation 元数据。它们都可能有 URI、ID 或文本字段,却不共享同一生命周期。
1. MCP能力类型
源码位置:codex-rs/protocol/src/mcp.rs :: McpServerInfo、Tool、Resource、ResourceContent
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct McpServerInfo {
pub name: String,
pub title: Option<String>,
pub version: String,
pub description: Option<String>,
pub icons: Option<Vec<serde_json::Value>>,
pub website_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct Tool {
pub name: String,
pub title: Option<String>,
pub description: Option<String>,
pub input_schema: serde_json::Value,
pub output_schema: Option<serde_json::Value>,
pub annotations: Option<serde_json::Value>,
pub icons: Option<Vec<serde_json::Value>>,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct Resource {
pub description: Option<String>,
pub mime_type: Option<String>,
pub name: String,
pub size: Option<i64>,
pub title: Option<String>,
pub uri: String,
pub meta: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(untagged)]
pub enum ResourceContent {
Text { uri: String, mime_type: Option<String>, text: String, meta: Option<serde_json::Value> },
Blob { uri: String, mime_type: Option<String>, blob: String, meta: Option<serde_json::Value> },
}server info 描述提供者,tool 描述可调用动作,resource 描述可读对象,resource content 才是一次读取的结果。resource 的 uri 是 MCP 外部身份,不应直接当作本机文件路径。
2. 资源来源
源码位置:codex-rs/protocol/src/mcp.rs :: McpResourceOriginCheckpoint、McpResourceOrigin、ClientMcpExtensions
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct McpResourceOriginCheckpoint {
pub origins: Vec<McpResourceOrigin>,
pub turns: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_turn_id: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct McpResourceOrigin {
pub call_id: String,
pub turn_id: Option<String>,
pub tool: String,
pub connector_id: String,
pub link_id: Option<String>,
pub uri: String,
pub ambiguous_account: bool,
}McpResourceOrigin 记录“哪个 tool call 为哪个 connector 的 URI 提供了授权来源”。它不是 resource content 本身,也不是普通 MCP call 的结果;compaction checkpoint 保存它,是为了后续 widget/resource 读取仍能找到来源。
源码位置:codex-rs/protocol/src/mcp.rs :: ClientMcpExtensions::for_mcp_servers
pub fn for_mcp_servers(&self) -> Self {
Self::new(
self.extensions
.iter()
.filter(|(id, _)| !MCP_CLIENT_ONLY_EXTENSION_IDS.contains(&id.as_str()))
.map(|(id, settings)| (id.clone(), settings.clone())),
)
}client-only extension 不会被广播给 MCP server。能力协商和客户端 UI 需要分开看,不能因为客户端声明了扩展就断言 server 收到了同样的字段。
3. MCP调用事件
源码位置:codex-rs/protocol/src/protocol.rs :: McpInvocation、McpToolCallBeginEvent、McpToolCallEndEvent
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
pub struct McpInvocation {
pub server: String,
pub tool: String,
pub arguments: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
pub struct McpToolCallBeginEvent {
pub call_id: String,
pub invocation: McpInvocation,
pub connector_id: Option<String>,
pub mcp_app_resource_uri: Option<String>,
pub link_id: Option<String>,
pub app_name: Option<String>,
pub action_name: Option<String>,
pub plugin_id: Option<String>,
pub read_only_hint: Option<bool>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS, PartialEq)]
pub struct McpToolCallEndEvent {
pub call_id: String,
pub invocation: McpInvocation,
pub connector_id: Option<String>,
pub mcp_app_resource_uri: Option<String>,
pub link_id: Option<String>,
pub app_name: Option<String>,
pub action_name: Option<String>,
pub plugin_id: Option<String>,
pub read_only_hint: Option<bool>,
pub duration: Duration,
pub result: Result<CallToolResult, String>,
}begin/end 用同一个 call_id 配对;end 的 result 可以是工具错误,read_only_hint 只是工具标注,不是执行结果。connector、app 和 plugin 字段让客户端能显示来源,但不改变 MCP server/tool 的基本调用语义。
源码位置:codex-rs/protocol/src/items.rs :: McpToolCallItem、McpToolCallStatus
pub struct McpToolCallItem {
pub id: String,
pub server: String,
pub tool: String,
pub arguments: serde_json::Value,
pub connector_id: Option<String>,
pub mcp_app_resource_uri: Option<String>,
pub link_id: Option<String>,
pub app_name: Option<String>,
pub action_name: Option<String>,
pub plugin_id: Option<String>,
pub read_only_hint: Option<bool>,
pub status: McpToolCallStatus,
pub result: Option<CallToolResult>,
pub error: Option<McpToolCallError>,
pub duration: Option<Duration>,
}
pub enum McpToolCallStatus {
InProgress,
Completed,
Failed,
}McpToolCallItem 是历史/UI 投影,比事件多了状态、错误和 duration。看到 Completed 只能说明 Core 收到了 end event 的成功结果,不能推断整个 turn 成功。
4. Resource read
源码位置:codex-rs/app-server-protocol/src/protocol/v2/mcp.rs :: McpResourceReadParams、McpResourceReadResponse
pub struct McpResourceReadParams {
pub thread_id: Option<String>,
pub origin_call_id: Option<String>,
pub server: String,
pub uri: String,
pub connector_id: Option<String>,
}
pub struct McpResourceReadResponse {
pub contents: Vec<McpResourceContent>,
pub origin_call_id: Option<String>,
}origin_call_id 是 0.150.0 中重要的关联字段:resource read 可以要求沿用某个 MCP tool call 的 app/account 来源。它与 thread_id 共同确定请求上下文,但并不替代 resource URI。
5. MemoryCitation
源码位置:codex-rs/protocol/src/memory_citation.rs :: MemoryCitation、MemoryCitationEntry
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct MemoryCitation {
pub entries: Vec<MemoryCitationEntry>,
pub rollout_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct MemoryCitationEntry {
pub path: String,
pub line_start: u32,
pub line_end: u32,
pub note: String,
}entries 面向可读引用,保存 path、行号和 note;rollout_ids 面向 memory store 关联。两组数据可以不一一对应,不能用展示 path 代替 rollout ID 做使用统计。
6. 隐藏标记解析
源码位置:codex-rs/core/src/stream_events_utils.rs :: strip_hidden_assistant_markup_and_parse_memory_citation
fn strip_hidden_assistant_markup_and_parse_memory_citation(
text: &str,
plan_mode: bool,
) -> (
String,
Option<codex_protocol::memory_citation::MemoryCitation>,
) {
let (without_citations, citations) = strip_citations(text);
let visible_text = if plan_mode {
strip_proposed_plan_blocks(&without_citations)
} else {
without_citations
};
(visible_text, parse_memory_citation(citations))
}解析先移除 citation,再按 plan mode 移除 proposed-plan block。普通文本用于 assistant message,citation 进入 FinalizedTurnItemFacts;解析失败不会回滚已经得到的可见文本。
源码位置:codex-rs/core/src/stream_events_utils.rs :: finalize_non_tool_response_item
let (memory_citation, last_agent_message, defers_mailbox_delivery_to_next_turn) =
match &turn_item {
TurnItem::AgentMessage(agent_message) => {
let combined = agent_message
.content
.iter()
.map(|entry| match entry {
codex_protocol::items::AgentMessageContent::Text { text } => text.as_str(),
})
.collect::<String>();
let last_agent_message = if combined.trim().is_empty() {
None
} else {
Some(combined)
};
// citation and mailbox facts continue in the source
(agent_message.memory_citation.clone(), last_agent_message, false)
}
_ => (None, None, false),
};最终 item facts 同时携带 citation 和 last message。citation 是 agent message 的附加事实,不是第二个 assistant message。
7. Usage accounting
源码位置:codex-rs/core/src/stream_events_utils.rs :: record_stage1_output_usage_for_memory_citation
async fn record_stage1_output_usage_for_memory_citation(
state_db_ctx: Option<&state_db::StateDbHandle>,
memory_citation: &MemoryCitation,
) -> bool {
let thread_ids = thread_ids_from_memory_citation(memory_citation);
if thread_ids.is_empty() {
return true;
}
if let Some(db) = state_db_ctx {
let _ = db.memories().record_stage1_output_usage(&thread_ids).await;
}
true
}没有可解析的 rollout IDs 时仍返回 true,因为可见响应不需要依赖 accounting 成功。数据库写入错误也不会让 assistant 文本消失;统计链是旁路消费者。
8. MCP与citation
MCP schema 或 resource content 解析失败会阻止该能力/结果进入对应协议层;MCP call end 的 Err(String) 会进入工具执行记录;citation 解析失败只影响隐藏元数据;memory store 写入失败只影响 usage accounting。把这些错误都归结为“模型响应失败”会误导排查。
9. 测试与边界
源码位置:codex-rs/core/src/stream_events_utils_tests.rs :: handle_non_tool_response_item_strips_citations_from_assistant_message、last_assistant_message_from_item_returns_none_for_citation_only_message
源码位置:codex-rs/app-server-protocol/src/protocol/v2/tests.rs :: mcp_server_elicitation_request_from_core_form_request、mcp_server_elicitation_response_round_trips_rmcp_result、MemoryCitation projection tests
源码位置:codex-rs/app-server-protocol/src/protocol/thread_history.rs :: handle_mcp_tool_call_begin、handle_mcp_tool_call_end
cd codex-rs
cargo test -p codex-core handle_non_tool_response_item_strips_citations_from_assistant_message -- --nocapture --test-threads=1
cargo test -p codex-core last_assistant_message_from_item_returns_none_for_citation_only_message -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol mcp_server_elicitation_request_from_core_form_request -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol mcp_server_elicitation_response_round_trips_rmcp_result -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol reconstructs_mcp_tool_result_meta_from_persisted_completion_events -- --nocapture --test-threads=1这些测试说明隐藏 citation 的可见文本处理、MCP elicitation 转换和工具历史投影;不能证明 MCP server 在线、resource origin 一定可恢复、memory store 写入成功或所有客户端都会展示 citation。
10. 源码定位练习
遇到 MCP 工具不显示,先检查 server/tool schema 适配,再查 runtime registry 和 McpToolCallBegin/End;遇到 resource read 关联错误,核对 origin_call_id、connector 和 URI,而不是只看 server 名称。
遇到 assistant 文本中出现奇怪的 citation 标记,沿 strip_citations → parse_memory_citation → FinalizedTurnItemFacts 追踪;遇到 memory usage 不变,检查 rollout IDs 和 state DB 旁路写入。可见文本、引用元数据和 MCP 外部资源必须分别定位。
