Skip to content

MCP与MemoryCitation类型

沿着 MCP 工具与资源、调用事件、资源来源和 MemoryCitation 解析,理解外部上下文如何进入会话。

基于rust-v0.150.0
CodexRustProtocolMCPMemory

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

rust
#[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

rust
#[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

rust
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

rust
#[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

rust
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

rust
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

rust
#[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

rust
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

rust
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

rust
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

text
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 外部资源必须分别定位。