mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Expose MCP server info as part of server status (#24698)
# Summary Expose MCP server info via App Server (when available) so apps can render a richer MCP experience
This commit is contained in:
committed by
GitHub
Unverified
parent
2a1158b8e2
commit
8a827d6426
+51
@@ -10964,6 +10964,47 @@
|
||||
"title": "McpResourceReadResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerInfo": {
|
||||
"description": "Presentation metadata advertised by an initialized MCP server.",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"icons": {
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"websiteUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"version"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerMigration": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -11074,6 +11115,16 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"serverInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/McpServerInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/v2/Tool"
|
||||
|
||||
+51
@@ -7493,6 +7493,47 @@
|
||||
"title": "McpResourceReadResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerInfo": {
|
||||
"description": "Presentation metadata advertised by an initialized MCP server.",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"icons": {
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"websiteUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"version"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerMigration": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@@ -7603,6 +7644,16 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"serverInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpServerInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Tool"
|
||||
|
||||
+51
@@ -10,6 +10,47 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"McpServerInfo": {
|
||||
"description": "Presentation metadata advertised by an initialized MCP server.",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"icons": {
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"websiteUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"version"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerStatus": {
|
||||
"properties": {
|
||||
"authStatus": {
|
||||
@@ -30,6 +71,16 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"serverInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/McpServerInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Tool"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "./serde_json/JsonValue";
|
||||
|
||||
/**
|
||||
* Presentation metadata advertised by an initialized MCP server.
|
||||
*/
|
||||
export type McpServerInfo = { name: string, title: string | null, version: string, description: string | null, icons: Array<JsonValue> | null, websiteUrl: string | null, };
|
||||
@@ -42,6 +42,7 @@ export type { InternalSessionSource } from "./InternalSessionSource";
|
||||
export type { LocalShellAction } from "./LocalShellAction";
|
||||
export type { LocalShellExecAction } from "./LocalShellExecAction";
|
||||
export type { LocalShellStatus } from "./LocalShellStatus";
|
||||
export type { McpServerInfo } from "./McpServerInfo";
|
||||
export type { MessagePhase } from "./MessagePhase";
|
||||
export type { ModeKind } from "./ModeKind";
|
||||
export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { McpServerInfo } from "../McpServerInfo";
|
||||
import type { Resource } from "../Resource";
|
||||
import type { ResourceTemplate } from "../ResourceTemplate";
|
||||
import type { Tool } from "../Tool";
|
||||
import type { McpAuthStatus } from "./McpAuthStatus";
|
||||
|
||||
export type McpServerStatus = { name: string, tools: { [key in string]?: Tool }, resources: Array<Resource>, resourceTemplates: Array<ResourceTemplate>, authStatus: McpAuthStatus, };
|
||||
export type McpServerStatus = { name: string, serverInfo: McpServerInfo | null, tools: { [key in string]?: Tool }, resources: Array<Resource>, resourceTemplates: Array<ResourceTemplate>, authStatus: McpAuthStatus, };
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::shared::v2_enum_from_core;
|
||||
use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest;
|
||||
use codex_protocol::items::McpToolCallError as CoreMcpToolCallError;
|
||||
use codex_protocol::mcp::CallToolResult as CoreMcpCallToolResult;
|
||||
use codex_protocol::mcp::McpServerInfo;
|
||||
use codex_protocol::mcp::Resource as McpResource;
|
||||
pub use codex_protocol::mcp::ResourceContent as McpResourceContent;
|
||||
use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate;
|
||||
@@ -53,6 +54,7 @@ pub enum McpServerStatusDetail {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpServerStatus {
|
||||
pub name: String,
|
||||
pub server_info: Option<McpServerInfo>,
|
||||
pub tools: std::collections::HashMap<String, McpTool>,
|
||||
pub resources: Vec<McpResource>,
|
||||
pub resource_templates: Vec<McpResourceTemplate>,
|
||||
|
||||
@@ -11,6 +11,7 @@ use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
use codex_protocol::items::WebSearchItem;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::mcp::McpServerInfo;
|
||||
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
|
||||
use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry;
|
||||
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
|
||||
@@ -2003,6 +2004,80 @@ fn mcp_server_elicitation_response_serializes_nullable_content() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_server_status_serializes_absent_server_info_as_null() {
|
||||
let response = ListMcpServerStatusResponse {
|
||||
data: vec![McpServerStatus {
|
||||
name: "not-ready".to_string(),
|
||||
server_info: None,
|
||||
tools: HashMap::new(),
|
||||
resources: Vec::new(),
|
||||
resource_templates: Vec::new(),
|
||||
auth_status: McpAuthStatus::Unsupported,
|
||||
}],
|
||||
next_cursor: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(response).expect("response should serialize"),
|
||||
json!({
|
||||
"data": [{
|
||||
"name": "not-ready",
|
||||
"serverInfo": null,
|
||||
"tools": {},
|
||||
"resources": [],
|
||||
"resourceTemplates": [],
|
||||
"authStatus": "unsupported",
|
||||
}],
|
||||
"nextCursor": null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_server_status_serializes_absent_server_info_metadata_as_null() {
|
||||
let response = ListMcpServerStatusResponse {
|
||||
data: vec![McpServerStatus {
|
||||
name: "initialized".to_string(),
|
||||
server_info: Some(McpServerInfo {
|
||||
name: "lookup-server".to_string(),
|
||||
title: None,
|
||||
version: "1.0.0".to_string(),
|
||||
description: None,
|
||||
icons: None,
|
||||
website_url: None,
|
||||
}),
|
||||
tools: HashMap::new(),
|
||||
resources: Vec::new(),
|
||||
resource_templates: Vec::new(),
|
||||
auth_status: McpAuthStatus::Unsupported,
|
||||
}],
|
||||
next_cursor: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(response).expect("response should serialize"),
|
||||
json!({
|
||||
"data": [{
|
||||
"name": "initialized",
|
||||
"serverInfo": {
|
||||
"name": "lookup-server",
|
||||
"title": null,
|
||||
"version": "1.0.0",
|
||||
"description": null,
|
||||
"icons": null,
|
||||
"websiteUrl": null,
|
||||
},
|
||||
"tools": {},
|
||||
"resources": [],
|
||||
"resourceTemplates": [],
|
||||
"authStatus": "unsupported",
|
||||
}],
|
||||
"nextCursor": null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_policy_round_trips_workspace_write_access() {
|
||||
let v2_policy = SandboxPolicy::WorkspaceWrite {
|
||||
|
||||
@@ -216,7 +216,7 @@ Example with notification opt-out:
|
||||
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
|
||||
- `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental).
|
||||
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
|
||||
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools and auth status, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`.
|
||||
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`.
|
||||
- `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, and `uri`, returning text/blob resource `contents`. If `threadId` is omitted, the server reads from the latest MCP config directly.
|
||||
- `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result.
|
||||
- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`.
|
||||
|
||||
@@ -276,6 +276,7 @@ impl McpRequestProcessor {
|
||||
.await;
|
||||
|
||||
let McpServerStatusSnapshot {
|
||||
server_infos,
|
||||
tools_by_server,
|
||||
resources,
|
||||
resource_templates,
|
||||
@@ -315,6 +316,7 @@ impl McpRequestProcessor {
|
||||
.iter()
|
||||
.map(|name| McpServerStatus {
|
||||
name: name.clone(),
|
||||
server_info: server_infos.get(name).cloned(),
|
||||
tools: tools_by_server.get(name).cloned().unwrap_or_default(),
|
||||
resources: resources.get(name).cloned().unwrap_or_default(),
|
||||
resource_templates: resource_templates.get(name).cloned().unwrap_or_default(),
|
||||
|
||||
@@ -20,6 +20,7 @@ use codex_core::config::set_project_trust_level;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::handler::server::ServerHandler;
|
||||
use rmcp::model::Implementation;
|
||||
use rmcp::model::JsonObject;
|
||||
use rmcp::model::ListResourceTemplatesResult;
|
||||
use rmcp::model::ListResourcesResult;
|
||||
@@ -99,6 +100,13 @@ url = "{mcp_server_url}/mcp"
|
||||
.map(|tool| tool.name.as_str()),
|
||||
Some("look-up.raw")
|
||||
);
|
||||
assert_eq!(
|
||||
status
|
||||
.server_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.title.as_deref()),
|
||||
Some("Lookup Server")
|
||||
);
|
||||
|
||||
mcp_server_handle.abort();
|
||||
let _ = mcp_server_handle.await;
|
||||
@@ -205,7 +213,9 @@ struct McpStatusServer {
|
||||
|
||||
impl ServerHandler for McpStatusServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build()).with_server_info(
|
||||
Implementation::new("lookup-server", "1.0.0").with_title("Lookup Server"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn list_tools(
|
||||
|
||||
@@ -12,7 +12,9 @@ use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::runtime::emit_duration;
|
||||
use crate::tools::MCP_TOOLS_CACHE_WRITE_DURATION_METRIC;
|
||||
use crate::tools::ToolInfo;
|
||||
use anyhow::Context;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::mcp::McpServerInfo;
|
||||
use codex_utils_plugins::mcp_connector::is_connector_id_allowed;
|
||||
use codex_utils_plugins::mcp_connector::sanitize_name;
|
||||
use serde::Deserialize;
|
||||
@@ -20,8 +22,6 @@ use serde::Serialize;
|
||||
use sha1::Digest;
|
||||
use sha1::Sha1;
|
||||
|
||||
pub(crate) const CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION: u8 = 3;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CodexAppsToolsCacheKey {
|
||||
pub(crate) account_id: Option<String>,
|
||||
@@ -44,11 +44,19 @@ pub(crate) struct CodexAppsToolsCacheContext {
|
||||
}
|
||||
|
||||
impl CodexAppsToolsCacheContext {
|
||||
pub(crate) fn cache_path(&self) -> PathBuf {
|
||||
pub(crate) fn tools_cache_path(&self) -> PathBuf {
|
||||
self.cache_path_in(CODEX_APPS_TOOLS_CACHE_DIR)
|
||||
}
|
||||
|
||||
pub(crate) fn server_info_cache_path(&self) -> PathBuf {
|
||||
self.cache_path_in(CODEX_APPS_SERVER_INFO_CACHE_DIR)
|
||||
}
|
||||
|
||||
fn cache_path_in(&self, cache_dir: &str) -> PathBuf {
|
||||
let user_key_json = serde_json::to_string(&self.user_key).unwrap_or_default();
|
||||
let user_key_hash = sha1_hex(&user_key_json);
|
||||
self.codex_home
|
||||
.join(CODEX_APPS_TOOLS_CACHE_DIR)
|
||||
.join(cache_dir)
|
||||
.join(format!("{user_key_hash}.json"))
|
||||
}
|
||||
}
|
||||
@@ -136,6 +144,7 @@ pub(crate) fn normalize_codex_apps_callable_namespace(
|
||||
pub(crate) fn write_cached_codex_apps_tools_if_needed(
|
||||
server_name: &str,
|
||||
cache_context: Option<&CodexAppsToolsCacheContext>,
|
||||
server_info: &McpServerInfo,
|
||||
tools: &[ToolInfo],
|
||||
) {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
@@ -145,6 +154,9 @@ pub(crate) fn write_cached_codex_apps_tools_if_needed(
|
||||
if let Some(cache_context) = cache_context {
|
||||
let cache_write_start = Instant::now();
|
||||
write_cached_codex_apps_tools(cache_context, tools);
|
||||
if let Err(err) = write_cached_codex_apps_server_info(cache_context, server_info) {
|
||||
tracing::warn!("failed to write Codex Apps server info cache: {err:#}");
|
||||
}
|
||||
emit_duration(
|
||||
MCP_TOOLS_CACHE_WRITE_DURATION_METRIC,
|
||||
cache_write_start.elapsed(),
|
||||
@@ -169,6 +181,17 @@ pub(crate) fn load_startup_cached_codex_apps_tools_snapshot(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_startup_cached_codex_apps_server_info(
|
||||
server_name: &str,
|
||||
cache_context: Option<&CodexAppsToolsCacheContext>,
|
||||
) -> Option<McpServerInfo> {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return None;
|
||||
}
|
||||
|
||||
load_cached_codex_apps_server_info(cache_context?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn read_cached_codex_apps_tools(
|
||||
cache_context: &CodexAppsToolsCacheContext,
|
||||
@@ -182,7 +205,7 @@ pub(crate) fn read_cached_codex_apps_tools(
|
||||
pub(crate) fn load_cached_codex_apps_tools(
|
||||
cache_context: &CodexAppsToolsCacheContext,
|
||||
) -> CachedCodexAppsToolsLoad {
|
||||
let cache_path = cache_context.cache_path();
|
||||
let cache_path = cache_context.tools_cache_path();
|
||||
let bytes = match std::fs::read(cache_path) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
@@ -204,7 +227,7 @@ pub(crate) fn write_cached_codex_apps_tools(
|
||||
cache_context: &CodexAppsToolsCacheContext,
|
||||
tools: &[ToolInfo],
|
||||
) {
|
||||
let cache_path = cache_context.cache_path();
|
||||
let cache_path = cache_context.tools_cache_path();
|
||||
if let Some(parent) = cache_path.parent()
|
||||
&& std::fs::create_dir_all(parent).is_err()
|
||||
{
|
||||
@@ -220,6 +243,42 @@ pub(crate) fn write_cached_codex_apps_tools(
|
||||
let _ = std::fs::write(cache_path, bytes);
|
||||
}
|
||||
|
||||
pub(crate) fn load_cached_codex_apps_server_info(
|
||||
cache_context: &CodexAppsToolsCacheContext,
|
||||
) -> Option<McpServerInfo> {
|
||||
let bytes = std::fs::read(cache_context.server_info_cache_path()).ok()?;
|
||||
let cache: CodexAppsServerInfoDiskCache = serde_json::from_slice(&bytes).ok()?;
|
||||
(cache.schema_version == CODEX_APPS_SERVER_INFO_CACHE_SCHEMA_VERSION)
|
||||
.then_some(cache.server_info)
|
||||
}
|
||||
|
||||
fn write_cached_codex_apps_server_info(
|
||||
cache_context: &CodexAppsToolsCacheContext,
|
||||
server_info: &McpServerInfo,
|
||||
) -> anyhow::Result<()> {
|
||||
let cache_path = cache_context.server_info_cache_path();
|
||||
if let Some(parent) = cache_path.parent() {
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!(
|
||||
"failed to create Codex Apps server info cache directory `{}`",
|
||||
parent.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
let bytes = serde_json::to_vec_pretty(&CodexAppsServerInfoDiskCache {
|
||||
schema_version: CODEX_APPS_SERVER_INFO_CACHE_SCHEMA_VERSION,
|
||||
server_info: server_info.clone(),
|
||||
})
|
||||
.context("failed to serialize Codex Apps server info cache")?;
|
||||
std::fs::write(&cache_path, bytes).with_context(|| {
|
||||
format!(
|
||||
"failed to write Codex Apps server info cache `{}`",
|
||||
cache_path.display()
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn filter_disallowed_codex_apps_tools(tools: Vec<ToolInfo>) -> Vec<ToolInfo> {
|
||||
tools
|
||||
.into_iter()
|
||||
@@ -237,7 +296,17 @@ struct CodexAppsToolsDiskCache {
|
||||
tools: Vec<ToolInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CodexAppsServerInfoDiskCache {
|
||||
schema_version: u8,
|
||||
server_info: McpServerInfo,
|
||||
}
|
||||
|
||||
const CODEX_APPS_TOOLS_CACHE_DIR: &str = "cache/codex_apps_tools";
|
||||
pub(crate) const CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION: u8 = 3;
|
||||
|
||||
const CODEX_APPS_SERVER_INFO_CACHE_DIR: &str = "cache/codex_apps_server_info";
|
||||
const CODEX_APPS_SERVER_INFO_CACHE_SCHEMA_VERSION: u8 = 1;
|
||||
|
||||
fn sha1_hex(s: &str) -> String {
|
||||
let mut hasher = Sha1::new();
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -44,6 +45,7 @@ use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::mcp::McpServerInfo;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::Event;
|
||||
@@ -385,13 +387,13 @@ impl McpConnectionManager {
|
||||
pub async fn list_all_tools(&self) -> Vec<ToolInfo> {
|
||||
let mut tools = Vec::new();
|
||||
for (server_name, managed_client) in &self.clients {
|
||||
let has_startup_snapshot = managed_client.startup_snapshot.is_some();
|
||||
let has_cached_tool_info_snapshot = managed_client.cached_tool_info_snapshot.is_some();
|
||||
let startup_complete = managed_client
|
||||
.startup_complete
|
||||
.load(std::sync::atomic::Ordering::Acquire);
|
||||
trace!(
|
||||
server_name = %server_name,
|
||||
has_startup_snapshot,
|
||||
has_cached_tool_info_snapshot,
|
||||
startup_complete,
|
||||
"waiting for MCP server tools while building tool list"
|
||||
);
|
||||
@@ -400,7 +402,7 @@ impl McpConnectionManager {
|
||||
.instrument(trace_span!(
|
||||
"list_tools_for_server",
|
||||
server_name = %server_name,
|
||||
has_startup_snapshot,
|
||||
has_cached_tool_info_snapshot,
|
||||
startup_complete
|
||||
))
|
||||
.await
|
||||
@@ -421,6 +423,31 @@ impl McpConnectionManager {
|
||||
normalize_tools_for_model_with_prefix(tools, self.prefix_mcp_tool_names)
|
||||
}
|
||||
|
||||
/// Returns presentation metadata without waiting for uncached clients still initializing.
|
||||
/// Cached values will be used if available and the server is still starting up.
|
||||
pub async fn list_available_server_infos(&self) -> HashMap<String, McpServerInfo> {
|
||||
let mut server_infos = HashMap::new();
|
||||
for (server_name, client) in &self.clients {
|
||||
if !client.startup_complete.load(Ordering::Acquire) {
|
||||
if let Some(server_info) = client.cached_server_info.clone() {
|
||||
server_infos.insert(server_name.clone(), server_info);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match client.client().await {
|
||||
Ok(managed_client) => {
|
||||
server_infos.insert(server_name.clone(), managed_client.server_info);
|
||||
}
|
||||
Err(_) => {
|
||||
if let Some(server_info) = client.cached_server_info.clone() {
|
||||
server_infos.insert(server_name.clone(), server_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
server_infos
|
||||
}
|
||||
|
||||
/// Force-refresh codex apps tools by bypassing the in-process cache.
|
||||
///
|
||||
/// On success, the refreshed tools replace the cache contents and the
|
||||
@@ -456,6 +483,7 @@ impl McpConnectionManager {
|
||||
write_cached_codex_apps_tools_if_needed(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
managed_client.codex_apps_tools_cache_context.as_ref(),
|
||||
&managed_client.server_info,
|
||||
&tools,
|
||||
);
|
||||
emit_duration(
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use super::*;
|
||||
use crate::codex_apps::CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION;
|
||||
use crate::codex_apps::CodexAppsToolsCacheContext;
|
||||
use crate::codex_apps::load_startup_cached_codex_apps_server_info;
|
||||
use crate::codex_apps::load_startup_cached_codex_apps_tools_snapshot;
|
||||
use crate::codex_apps::read_cached_codex_apps_tools;
|
||||
use crate::codex_apps::write_cached_codex_apps_tools;
|
||||
use crate::codex_apps::write_cached_codex_apps_tools_if_needed;
|
||||
use crate::declared_openai_file_input_param_names;
|
||||
use crate::elicitation::ElicitationRequestManager;
|
||||
use crate::elicitation::elicitation_is_rejected_by_policy;
|
||||
@@ -20,6 +22,7 @@ use codex_config::Constrained;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_protocol::ToolName;
|
||||
use codex_protocol::mcp::McpServerInfo;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use codex_protocol::protocol::McpAuthStatus;
|
||||
@@ -82,6 +85,17 @@ fn create_codex_apps_tools_cache_context(
|
||||
}
|
||||
}
|
||||
|
||||
fn create_test_server_info(title: &str) -> McpServerInfo {
|
||||
McpServerInfo {
|
||||
name: "codex-apps".to_string(),
|
||||
title: Some(title.to_string()),
|
||||
version: "1.0.0".to_string(),
|
||||
description: None,
|
||||
icons: None,
|
||||
website_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn model_tool_names(tools: &[ToolInfo]) -> HashSet<ToolName> {
|
||||
tools
|
||||
.iter()
|
||||
@@ -582,8 +596,8 @@ fn codex_apps_tools_cache_is_scoped_per_user() {
|
||||
assert_eq!(read_user_1[0].callable_name, "one");
|
||||
assert_eq!(read_user_2[0].callable_name, "two");
|
||||
assert_ne!(
|
||||
cache_context_user_1.cache_path(),
|
||||
cache_context_user_2.cache_path(),
|
||||
cache_context_user_1.tools_cache_path(),
|
||||
cache_context_user_2.tools_cache_path(),
|
||||
"each user should get an isolated cache file"
|
||||
);
|
||||
}
|
||||
@@ -627,7 +641,7 @@ fn codex_apps_tools_cache_is_ignored_when_schema_version_mismatches() {
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
let cache_path = cache_context.cache_path();
|
||||
let cache_path = cache_context.tools_cache_path();
|
||||
if let Some(parent) = cache_path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("create parent");
|
||||
}
|
||||
@@ -649,7 +663,7 @@ fn codex_apps_tools_cache_is_ignored_when_json_is_invalid() {
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
let cache_path = cache_context.cache_path();
|
||||
let cache_path = cache_context.tools_cache_path();
|
||||
if let Some(parent) = cache_path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("create parent");
|
||||
}
|
||||
@@ -670,21 +684,112 @@ fn startup_cached_codex_apps_tools_loads_from_disk_cache() {
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_search",
|
||||
)];
|
||||
write_cached_codex_apps_tools(&cache_context, &cached_tools);
|
||||
let server_info = create_test_server_info("Codex Apps");
|
||||
write_cached_codex_apps_tools_if_needed(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
&server_info,
|
||||
&cached_tools,
|
||||
);
|
||||
|
||||
let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot(
|
||||
let startup_tools = load_startup_cached_codex_apps_tools_snapshot(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
)
|
||||
.expect("expected startup snapshot to load from cache");
|
||||
let cached_server_info = load_startup_cached_codex_apps_server_info(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
);
|
||||
let startup_tools = startup_snapshot.expect("expected startup snapshot to load from cache");
|
||||
|
||||
assert_eq!(startup_tools.len(), 1);
|
||||
assert_eq!(startup_tools[0].server_name, CODEX_APPS_MCP_SERVER_NAME);
|
||||
assert_eq!(startup_tools[0].callable_name, "calendar_search");
|
||||
assert_eq!(cached_server_info, Some(server_info));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_cached_codex_apps_tools_loads_without_server_info_cache() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache_context = create_codex_apps_tools_cache_context(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
let cache_path = cache_context.tools_cache_path();
|
||||
if let Some(parent) = cache_path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("create parent");
|
||||
}
|
||||
let bytes = serde_json::to_vec_pretty(&serde_json::json!({
|
||||
"schema_version": CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION,
|
||||
"tools": [create_test_tool(CODEX_APPS_MCP_SERVER_NAME, "calendar_search")],
|
||||
}))
|
||||
.expect("serialize");
|
||||
std::fs::write(cache_path, bytes).expect("write");
|
||||
|
||||
let startup_tools = load_startup_cached_codex_apps_tools_snapshot(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
)
|
||||
.expect("legacy startup snapshot should remain available");
|
||||
let cached_server_info = load_startup_cached_codex_apps_server_info(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
);
|
||||
|
||||
assert_eq!(startup_tools.len(), 1);
|
||||
assert_eq!(startup_tools[0].callable_name, "calendar_search");
|
||||
assert_eq!(cached_server_info, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_server_info_cache_survives_legacy_tools_cache_write() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let cache_context = create_codex_apps_tools_cache_context(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some("account-one"),
|
||||
Some("user-one"),
|
||||
);
|
||||
let server_info = create_test_server_info("Codex Apps");
|
||||
write_cached_codex_apps_tools_if_needed(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
&server_info,
|
||||
&[create_test_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_search",
|
||||
)],
|
||||
);
|
||||
|
||||
let cache_path = cache_context.tools_cache_path();
|
||||
if let Some(parent) = cache_path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("create parent");
|
||||
}
|
||||
let bytes = serde_json::to_vec_pretty(&serde_json::json!({
|
||||
"schema_version": CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION - 1,
|
||||
"tools": [create_test_tool(CODEX_APPS_MCP_SERVER_NAME, "calendar_search")],
|
||||
}))
|
||||
.expect("serialize");
|
||||
std::fs::write(cache_path, bytes).expect("write legacy tools cache");
|
||||
|
||||
assert_eq!(
|
||||
load_startup_cached_codex_apps_server_info(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
),
|
||||
Some(server_info)
|
||||
);
|
||||
assert!(
|
||||
load_startup_cached_codex_apps_tools_snapshot(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&cache_context),
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() {
|
||||
async fn list_all_tools_uses_cached_tool_info_snapshot_while_client_is_pending() {
|
||||
let startup_tools = vec![create_test_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
@@ -703,7 +808,8 @@ async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() {
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
startup_snapshot: Some(startup_tools),
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
@@ -722,6 +828,43 @@ async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() {
|
||||
assert_eq!(tool.callable_name, "calendar_create_event");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_available_server_infos_uses_cache_while_client_is_pending() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
&permission_profile,
|
||||
/*prefix_mcp_tool_names*/ true,
|
||||
);
|
||||
let server_info = create_test_server_info("Codex Apps");
|
||||
manager.clients.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
cached_tool_info_snapshot: Some(Vec::new()),
|
||||
cached_server_info: Some(server_info.clone()),
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
},
|
||||
);
|
||||
|
||||
let timeout_result = tokio::time::timeout(
|
||||
Duration::from_millis(10),
|
||||
manager.list_available_server_infos(),
|
||||
)
|
||||
.await;
|
||||
let server_infos = timeout_result.expect("server info lookup should not block on startup");
|
||||
assert_eq!(
|
||||
server_infos.get(CODEX_APPS_MCP_SERVER_NAME),
|
||||
Some(&server_info)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_accepts_canonical_namespaced_tool_names() {
|
||||
let startup_tools = vec![create_test_tool("rmcp", "echo")];
|
||||
@@ -739,7 +882,8 @@ async fn list_all_tools_accepts_canonical_namespaced_tool_names() {
|
||||
"rmcp".to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
startup_snapshot: Some(startup_tools),
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
@@ -781,7 +925,8 @@ async fn list_all_tools_applies_legacy_mcp_prefix_by_default() {
|
||||
"rmcp".to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
startup_snapshot: Some(startup_tools),
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
@@ -807,7 +952,7 @@ async fn list_all_tools_applies_legacy_mcp_prefix_by_default() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot() {
|
||||
async fn list_all_tools_blocks_while_client_is_pending_without_cached_tool_info_snapshot() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
@@ -822,7 +967,8 @@ async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
startup_snapshot: None,
|
||||
cached_tool_info_snapshot: None,
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
@@ -835,7 +981,7 @@ async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot(
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty() {
|
||||
async fn list_all_tools_does_not_block_when_cached_tool_info_snapshot_is_empty() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
@@ -850,7 +996,8 @@ async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
startup_snapshot: Some(Vec::new()),
|
||||
cached_tool_info_snapshot: Some(Vec::new()),
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
@@ -864,11 +1011,12 @@ async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty(
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
|
||||
async fn list_all_tools_uses_cached_tool_info_snapshot_when_client_startup_fails() {
|
||||
let startup_tools = vec![create_test_tool(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"calendar_create_event",
|
||||
)];
|
||||
let server_info = create_test_server_info("Codex Apps");
|
||||
let failed_client = futures::future::ready::<Result<ManagedClient, StartupOutcomeError>>(Err(
|
||||
StartupOutcomeError::Failed {
|
||||
error: "startup failed".to_string(),
|
||||
@@ -888,7 +1036,8 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: failed_client,
|
||||
startup_snapshot: Some(startup_tools),
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: Some(server_info.clone()),
|
||||
startup_complete,
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
@@ -905,6 +1054,13 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
|
||||
.expect("tool from startup cache");
|
||||
assert_eq!(tool.server_name, CODEX_APPS_MCP_SERVER_NAME);
|
||||
assert_eq!(tool.callable_name, "calendar_create_event");
|
||||
assert_eq!(
|
||||
manager
|
||||
.list_available_server_infos()
|
||||
.await
|
||||
.get(CODEX_APPS_MCP_SERVER_NAME),
|
||||
Some(&server_info)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -935,7 +1091,8 @@ async fn list_all_tools_adds_server_metadata_to_cached_tools() {
|
||||
server_name.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: pending_client,
|
||||
startup_snapshot: Some(startup_tools),
|
||||
cached_tool_info_snapshot: Some(startup_tools),
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
|
||||
@@ -25,6 +25,7 @@ use codex_config::types::ApprovalsReviewer;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_plugin::PluginCapabilitySummary;
|
||||
use codex_protocol::mcp::McpServerInfo;
|
||||
use codex_protocol::mcp::Resource;
|
||||
use codex_protocol::mcp::ResourceTemplate;
|
||||
use codex_protocol::mcp::Tool;
|
||||
@@ -308,6 +309,7 @@ pub async fn read_mcp_resource(
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct McpServerStatusSnapshot {
|
||||
pub server_infos: HashMap<String, McpServerInfo>,
|
||||
pub tools_by_server: HashMap<String, HashMap<String, Tool>>,
|
||||
pub resources: HashMap<String, Vec<Resource>>,
|
||||
pub resource_templates: HashMap<String, Vec<ResourceTemplate>>,
|
||||
@@ -327,6 +329,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
let tool_plugin_provenance = tool_plugin_provenance(config);
|
||||
if mcp_servers.is_empty() {
|
||||
return McpServerStatusSnapshot {
|
||||
server_infos: HashMap::new(),
|
||||
tools_by_server: HashMap::new(),
|
||||
resources: HashMap::new(),
|
||||
resource_templates: HashMap::new(),
|
||||
@@ -599,6 +602,7 @@ async fn collect_mcp_server_status_snapshot_from_manager(
|
||||
}
|
||||
},
|
||||
);
|
||||
let server_infos = mcp_connection_manager.list_available_server_infos().await;
|
||||
|
||||
let mut tools_by_server = HashMap::<String, HashMap<String, Tool>>::new();
|
||||
for tool_info in tools {
|
||||
@@ -614,6 +618,7 @@ async fn collect_mcp_server_status_snapshot_from_manager(
|
||||
}
|
||||
|
||||
McpServerStatusSnapshot {
|
||||
server_infos,
|
||||
tools_by_server,
|
||||
resources: convert_mcp_resources(resources),
|
||||
resource_templates: convert_mcp_resource_templates(resource_templates),
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::codex_apps::CachedCodexAppsToolsLoad;
|
||||
use crate::codex_apps::CodexAppsToolsCacheContext;
|
||||
use crate::codex_apps::filter_disallowed_codex_apps_tools;
|
||||
use crate::codex_apps::load_cached_codex_apps_tools;
|
||||
use crate::codex_apps::load_startup_cached_codex_apps_server_info;
|
||||
use crate::codex_apps::load_startup_cached_codex_apps_tools_snapshot;
|
||||
use crate::codex_apps::normalize_codex_apps_callable_name;
|
||||
use crate::codex_apps::normalize_codex_apps_callable_namespace;
|
||||
@@ -47,6 +48,7 @@ use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_exec_server::HttpClient;
|
||||
use codex_exec_server::ReqwestHttpClient;
|
||||
use codex_protocol::mcp::McpServerInfo;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_rmcp_client::ExecutorStdioServerLauncher;
|
||||
use codex_rmcp_client::LocalStdioServerLauncher;
|
||||
@@ -85,6 +87,7 @@ const UNTRUSTED_CONNECTOR_META_KEYS: &[&str] = &[
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ManagedClient {
|
||||
pub(crate) client: Arc<RmcpClient>,
|
||||
pub(crate) server_info: McpServerInfo,
|
||||
pub(crate) tools: Vec<ToolInfo>,
|
||||
pub(crate) tool_filter: ToolFilter,
|
||||
pub(crate) tool_timeout: Option<Duration>,
|
||||
@@ -123,7 +126,8 @@ impl ManagedClient {
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AsyncManagedClient {
|
||||
pub(crate) client: Shared<BoxFuture<'static, Result<ManagedClient, StartupOutcomeError>>>,
|
||||
pub(crate) startup_snapshot: Option<Vec<ToolInfo>>,
|
||||
pub(crate) cached_tool_info_snapshot: Option<Vec<ToolInfo>>,
|
||||
pub(crate) cached_server_info: Option<McpServerInfo>,
|
||||
pub(crate) startup_complete: Arc<AtomicBool>,
|
||||
pub(crate) tool_plugin_provenance: Arc<ToolPluginProvenance>,
|
||||
pub(crate) cancel_token: CancellationToken,
|
||||
@@ -150,11 +154,16 @@ impl AsyncManagedClient {
|
||||
.configured_config()
|
||||
.map(ToolFilter::from_config)
|
||||
.unwrap_or_default();
|
||||
let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot(
|
||||
let cached_tool_info_snapshot = load_startup_cached_codex_apps_tools_snapshot(
|
||||
&server_name,
|
||||
codex_apps_tools_cache_context.as_ref(),
|
||||
)
|
||||
.map(|tools| filter_tools(tools, &tool_filter));
|
||||
);
|
||||
let cached_tool_info_snapshot =
|
||||
cached_tool_info_snapshot.map(|tools| filter_tools(tools, &tool_filter));
|
||||
let cached_server_info = load_startup_cached_codex_apps_server_info(
|
||||
&server_name,
|
||||
codex_apps_tools_cache_context.as_ref(),
|
||||
);
|
||||
let startup_tool_filter = tool_filter;
|
||||
let startup_complete = Arc::new(AtomicBool::new(false));
|
||||
let startup_complete_for_fut = Arc::clone(&startup_complete);
|
||||
@@ -207,7 +216,7 @@ impl AsyncManagedClient {
|
||||
outcome
|
||||
};
|
||||
let client = fut.boxed().shared();
|
||||
if startup_snapshot.is_some() {
|
||||
if cached_tool_info_snapshot.is_some() {
|
||||
let startup_task = client.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = startup_task.await;
|
||||
@@ -216,7 +225,8 @@ impl AsyncManagedClient {
|
||||
|
||||
Self {
|
||||
client,
|
||||
startup_snapshot,
|
||||
cached_tool_info_snapshot,
|
||||
cached_server_info,
|
||||
startup_complete,
|
||||
tool_plugin_provenance,
|
||||
cancel_token,
|
||||
@@ -238,9 +248,9 @@ impl AsyncManagedClient {
|
||||
}
|
||||
}
|
||||
|
||||
fn startup_snapshot_while_initializing(&self) -> Option<Vec<ToolInfo>> {
|
||||
fn cached_tool_info_snapshot_while_initializing(&self) -> Option<Vec<ToolInfo>> {
|
||||
if !self.startup_complete.load(Ordering::Acquire) {
|
||||
return self.startup_snapshot.clone();
|
||||
return self.cached_tool_info_snapshot.clone();
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -298,12 +308,13 @@ impl AsyncManagedClient {
|
||||
};
|
||||
|
||||
// Keep cache payloads raw; plugin provenance is resolved per-session at read time.
|
||||
let tools = if let Some(startup_tools) = self.startup_snapshot_while_initializing() {
|
||||
let tools = if let Some(startup_tools) = self.cached_tool_info_snapshot_while_initializing()
|
||||
{
|
||||
Some(startup_tools)
|
||||
} else {
|
||||
match self.client().await {
|
||||
Ok(client) => Some(client.listed_tools()),
|
||||
Err(_) => self.startup_snapshot.clone(),
|
||||
Err(_) => self.cached_tool_info_snapshot.clone(),
|
||||
}
|
||||
};
|
||||
tools.map(annotate_tools)
|
||||
@@ -506,9 +517,11 @@ async fn start_server_task(
|
||||
fetch_start.elapsed(),
|
||||
&[],
|
||||
);
|
||||
let server_info = mcp_server_info_from_implementation(initialize_result.server_info);
|
||||
write_cached_codex_apps_tools_if_needed(
|
||||
&server_name,
|
||||
codex_apps_tools_cache_context.as_ref(),
|
||||
&server_info,
|
||||
&tools,
|
||||
);
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
@@ -522,6 +535,7 @@ async fn start_server_task(
|
||||
|
||||
let managed = ManagedClient {
|
||||
client: Arc::clone(&client),
|
||||
server_info,
|
||||
tools,
|
||||
tool_timeout: Some(tool_timeout),
|
||||
tool_filter,
|
||||
@@ -533,6 +547,22 @@ async fn start_server_task(
|
||||
Ok(managed)
|
||||
}
|
||||
|
||||
fn mcp_server_info_from_implementation(server_info: Implementation) -> McpServerInfo {
|
||||
McpServerInfo {
|
||||
name: server_info.name,
|
||||
title: server_info.title,
|
||||
version: server_info.version,
|
||||
description: server_info.description,
|
||||
icons: server_info.icons.map(|icons| {
|
||||
icons
|
||||
.into_iter()
|
||||
.filter_map(|icon| serde_json::to_value(icon).ok())
|
||||
.collect()
|
||||
}),
|
||||
website_url: server_info.website_url,
|
||||
}
|
||||
}
|
||||
|
||||
struct StartServerTaskParams {
|
||||
startup_timeout: Option<Duration>, // TODO: cancel_token should handle this.
|
||||
tool_timeout: Duration,
|
||||
|
||||
@@ -26,6 +26,18 @@ impl std::fmt::Display for RequestId {
|
||||
}
|
||||
}
|
||||
|
||||
/// Presentation metadata advertised by an initialized MCP server.
|
||||
#[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>,
|
||||
}
|
||||
|
||||
/// Definition for a tool the client can call.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -1061,6 +1061,7 @@ mod tests {
|
||||
let statuses = vec![
|
||||
McpServerStatus {
|
||||
name: "docs".to_string(),
|
||||
server_info: None,
|
||||
tools: HashMap::from([(
|
||||
"list".to_string(),
|
||||
Tool {
|
||||
@@ -1080,6 +1081,7 @@ mod tests {
|
||||
},
|
||||
McpServerStatus {
|
||||
name: "disabled".to_string(),
|
||||
server_info: None,
|
||||
tools: HashMap::new(),
|
||||
resources: Vec::new(),
|
||||
resource_templates: Vec::new(),
|
||||
|
||||
@@ -147,6 +147,7 @@ async fn handle_mcp_inventory_result_respects_origin_thread() {
|
||||
app.handle_mcp_inventory_result(
|
||||
Ok(vec![McpServerStatus {
|
||||
name: "docs".to_string(),
|
||||
server_info: None,
|
||||
tools: HashMap::new(),
|
||||
resources: Vec::new(),
|
||||
resource_templates: Vec::new(),
|
||||
|
||||
@@ -843,6 +843,7 @@ async fn mcp_tools_output_lists_tools_for_hyphenated_server_names() {
|
||||
fn mcp_tools_output_from_statuses_renders_status_only_servers() {
|
||||
let statuses = vec![McpServerStatus {
|
||||
name: "plugin_docs".to_string(),
|
||||
server_info: None,
|
||||
tools: HashMap::from([(
|
||||
"lookup".to_string(),
|
||||
Tool {
|
||||
@@ -872,6 +873,7 @@ fn mcp_tools_output_from_statuses_renders_status_only_servers() {
|
||||
fn mcp_tools_output_from_statuses_renders_verbose_inventory() {
|
||||
let statuses = vec![McpServerStatus {
|
||||
name: "plugin_docs".to_string(),
|
||||
server_info: None,
|
||||
tools: HashMap::from([(
|
||||
"lookup".to_string(),
|
||||
Tool {
|
||||
|
||||
Reference in New Issue
Block a user