Compare commits

...

1 Commits

Author SHA1 Message Date
rreichel3-oai
d1934e7123 Expose app tool schemas in app list
Add AppToolInfo to the v2 app-server protocol and include effective MCP tool input schemas on AppInfo.

Carry accessible connector tool schemas through connector collection and directory merge paths, and document tools[].inputSchema in app/list.

Update generated protocol schemas and app/list expectations for the new tools field.
2026-04-07 12:33:08 -04:00
26 changed files with 346 additions and 6 deletions

View File

@@ -215,6 +215,13 @@
"type": "string"
},
"type": "array"
},
"tools": {
"default": [],
"items": {
"$ref": "#/definitions/AppToolInfo"
},
"type": "array"
}
},
"required": [
@@ -362,6 +369,26 @@
],
"type": "object"
},
"AppToolInfo": {
"description": "EXPERIMENTAL - app tool metadata returned by app-list APIs.\n\n`input_schema` is the effective JSON Schema Codex will use for this tool's parameters, including any connector link parameter constraints.",
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"inputSchema": true,
"name": {
"type": "string"
}
},
"required": [
"inputSchema",
"name"
],
"type": "object"
},
"AuthMode": {
"description": "Authentication mode for OpenAI-backed providers.",
"oneOf": [

View File

@@ -5127,6 +5127,13 @@
"type": "string"
},
"type": "array"
},
"tools": {
"default": [],
"items": {
"$ref": "#/definitions/v2/AppToolInfo"
},
"type": "array"
}
},
"required": [
@@ -5337,6 +5344,26 @@
},
"type": "object"
},
"AppToolInfo": {
"description": "EXPERIMENTAL - app tool metadata returned by app-list APIs.\n\n`input_schema` is the effective JSON Schema Codex will use for this tool's parameters, including any connector link parameter constraints.",
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"inputSchema": true,
"name": {
"type": "string"
}
},
"required": [
"inputSchema",
"name"
],
"type": "object"
},
"AppToolsConfig": {
"type": "object"
},

View File

@@ -326,6 +326,13 @@
"type": "string"
},
"type": "array"
},
"tools": {
"default": [],
"items": {
"$ref": "#/definitions/AppToolInfo"
},
"type": "array"
}
},
"required": [
@@ -536,6 +543,26 @@
},
"type": "object"
},
"AppToolInfo": {
"description": "EXPERIMENTAL - app tool metadata returned by app-list APIs.\n\n`input_schema` is the effective JSON Schema Codex will use for this tool's parameters, including any connector link parameter constraints.",
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"inputSchema": true,
"name": {
"type": "string"
}
},
"required": [
"inputSchema",
"name"
],
"type": "object"
},
"AppToolsConfig": {
"type": "object"
},

View File

@@ -126,6 +126,13 @@
"type": "string"
},
"type": "array"
},
"tools": {
"default": [],
"items": {
"$ref": "#/definitions/AppToolInfo"
},
"type": "array"
}
},
"required": [
@@ -257,6 +264,26 @@
"userPrompt"
],
"type": "object"
},
"AppToolInfo": {
"description": "EXPERIMENTAL - app tool metadata returned by app-list APIs.\n\n`input_schema` is the effective JSON Schema Codex will use for this tool's parameters, including any connector link parameter constraints.",
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"inputSchema": true,
"name": {
"type": "string"
}
},
"required": [
"inputSchema",
"name"
],
"type": "object"
}
},
"description": "EXPERIMENTAL - notification emitted when the app list changes.",

View File

@@ -126,6 +126,13 @@
"type": "string"
},
"type": "array"
},
"tools": {
"default": [],
"items": {
"$ref": "#/definitions/AppToolInfo"
},
"type": "array"
}
},
"required": [
@@ -257,6 +264,26 @@
"userPrompt"
],
"type": "object"
},
"AppToolInfo": {
"description": "EXPERIMENTAL - app tool metadata returned by app-list APIs.\n\n`input_schema` is the effective JSON Schema Codex will use for this tool's parameters, including any connector link parameter constraints.",
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"inputSchema": true,
"name": {
"type": "string"
}
},
"required": [
"inputSchema",
"name"
],
"type": "object"
}
},
"description": "EXPERIMENTAL - app list response.",

View File

@@ -3,6 +3,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AppBranding } from "./AppBranding";
import type { AppMetadata } from "./AppMetadata";
import type { AppToolInfo } from "./AppToolInfo";
/**
* EXPERIMENTAL - app metadata returned by app-list APIs.
@@ -16,4 +17,4 @@ export type AppInfo = { id: string, name: string, description: string | null, lo
* enabled = false
* ```
*/
isEnabled: boolean, pluginDisplayNames: Array<string>, };
isEnabled: boolean, pluginDisplayNames: Array<string>, tools: Array<AppToolInfo>, };

View File

@@ -0,0 +1,12 @@
// 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";
/**
* EXPERIMENTAL - app tool metadata returned by app-list APIs.
*
* `input_schema` is the effective JSON Schema Codex will use for this
* tool's parameters, including any connector link parameter constraints.
*/
export type AppToolInfo = { name: string, description: string | null, inputSchema: JsonValue, };

View File

@@ -17,6 +17,7 @@ export type { AppReview } from "./AppReview";
export type { AppScreenshot } from "./AppScreenshot";
export type { AppSummary } from "./AppSummary";
export type { AppToolApproval } from "./AppToolApproval";
export type { AppToolInfo } from "./AppToolInfo";
export type { AppToolsConfig } from "./AppToolsConfig";
export type { ApprovalsReviewer } from "./ApprovalsReviewer";
export type { AppsConfig } from "./AppsConfig";

View File

@@ -2070,6 +2070,19 @@ pub struct AppMetadata {
pub show_in_composer_when_unlinked: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
/// EXPERIMENTAL - app tool metadata returned by app-list APIs.
///
/// `input_schema` is the effective JSON Schema Codex will use for this
/// tool's parameters, including any connector link parameter constraints.
pub struct AppToolInfo {
pub name: String,
pub description: Option<String>,
pub input_schema: JsonValue,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -2097,6 +2110,8 @@ pub struct AppInfo {
pub is_enabled: bool,
#[serde(default)]
pub plugin_display_names: Vec<String>,
#[serde(default)]
pub tools: Vec<AppToolInfo>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]

View File

@@ -1217,7 +1217,9 @@ To enable or disable a skill by name:
## Apps
Use `app/list` to fetch available apps (connectors). Each entry includes metadata like the app `id`, display `name`, `installUrl`, `branding`, `appMetadata`, `labels`, whether it is currently accessible, and whether it is enabled in config.
Use `app/list` to fetch available apps (connectors). Each entry includes metadata like the app `id`, display `name`, `installUrl`, `branding`, `appMetadata`, `labels`, whether it is currently accessible, whether it is enabled in config, and the app tools currently available to Codex.
For accessible apps, `tools[].inputSchema` is the effective tool parameter JSON Schema Codex will use, including any connector link parameter constraints. Directory-only apps do not have link-specific tool schemas and return an empty `tools` list.
```json
{ "method": "app/list", "id": 50, "params": {
@@ -1240,7 +1242,22 @@ Use `app/list` to fetch available apps (connectors). Each entry includes metadat
"labels": null,
"installUrl": "https://chatgpt.com/apps/demo-app/demo-app",
"isAccessible": true,
"isEnabled": true
"isEnabled": true,
"pluginDisplayNames": [],
"tools": [
{
"name": "search",
"description": "Search the demo app.",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" }
},
"required": ["query"],
"additionalProperties": false
}
}
]
}
],
"nextCursor": null
@@ -1270,7 +1287,22 @@ The server also emits `app/list/updated` notifications whenever either source (a
"labels": null,
"installUrl": "https://chatgpt.com/apps/demo-app/demo-app",
"isAccessible": true,
"isEnabled": true
"isEnabled": true,
"pluginDisplayNames": [],
"tools": [
{
"name": "search",
"description": "Search the demo app.",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" }
},
"required": ["query"],
"additionalProperties": false
}
}
]
}
]
}

View File

@@ -130,6 +130,7 @@ mod tests {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
assert_eq!(

View File

@@ -25,6 +25,7 @@ use codex_app_server_protocol::AppListUpdatedNotification;
use codex_app_server_protocol::AppMetadata;
use codex_app_server_protocol::AppReview;
use codex_app_server_protocol::AppScreenshot;
use codex_app_server_protocol::AppToolInfo;
use codex_app_server_protocol::AppsListParams;
use codex_app_server_protocol::AppsListResponse;
use codex_app_server_protocol::AuthMode;
@@ -103,6 +104,7 @@ async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
let tools = vec![connector_tool("beta", "Beta App")?];
let (server_url, server_handle) =
@@ -164,6 +166,7 @@ async fn list_apps_uses_thread_feature_flag_when_thread_id_is_provided() -> Resu
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
let tools = vec![connector_tool("beta", "Beta App")?];
let (server_url, server_handle) =
@@ -267,6 +270,7 @@ async fn list_apps_reports_is_enabled_from_config() -> Result<()> {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
let tools = vec![connector_tool("beta", "Beta App")?];
let (server_url, server_handle) =
@@ -377,6 +381,7 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
AppInfo {
id: "beta".to_string(),
@@ -392,6 +397,7 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
];
@@ -441,6 +447,7 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: expected_connector_tools("beta"),
}];
let first_update = read_app_list_updated_notification(&mut mcp).await?;
@@ -461,6 +468,7 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: expected_connector_tools("beta"),
},
AppInfo {
id: "alpha".to_string(),
@@ -476,6 +484,7 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
];
@@ -517,6 +526,7 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates()
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
AppInfo {
id: "beta".to_string(),
@@ -532,6 +542,7 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates()
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
];
@@ -582,6 +593,7 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates()
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: expected_connector_tools("beta"),
},
AppInfo {
id: "alpha".to_string(),
@@ -597,6 +609,7 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates()
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
];
@@ -641,6 +654,7 @@ async fn list_apps_does_not_emit_empty_interim_updates() -> Result<()> {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
let (server_url, server_handle) = start_apps_server_with_delays(
connectors.clone(),
@@ -697,6 +711,7 @@ async fn list_apps_does_not_emit_empty_interim_updates() -> Result<()> {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
let update = read_app_list_updated_notification(&mut mcp).await?;
@@ -732,6 +747,7 @@ async fn list_apps_paginates_results() -> Result<()> {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
AppInfo {
id: "beta".to_string(),
@@ -747,6 +763,7 @@ async fn list_apps_paginates_results() -> Result<()> {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
];
@@ -805,6 +822,7 @@ async fn list_apps_paginates_results() -> Result<()> {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: expected_connector_tools("beta"),
}];
assert_eq!(first_page, expected_first);
@@ -849,6 +867,7 @@ async fn list_apps_paginates_results() -> Result<()> {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
assert_eq!(second_page, expected_second);
@@ -874,6 +893,7 @@ async fn list_apps_force_refetch_preserves_previous_cache_on_failure() -> Result
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
let tools = vec![connector_tool("beta", "Beta App")?];
let (server_url, server_handle) =
@@ -979,6 +999,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
AppInfo {
id: "beta".to_string(),
@@ -994,6 +1015,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
];
let initial_tools = vec![connector_tool("beta", "Beta App")?];
@@ -1044,6 +1066,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: expected_connector_tools("beta"),
}]
);
@@ -1065,6 +1088,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: expected_connector_tools("beta"),
},
AppInfo {
id: "alpha".to_string(),
@@ -1080,6 +1104,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
]
);
@@ -1110,6 +1135,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}]);
server_control.set_tools(Vec::new());
@@ -1140,6 +1166,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: expected_connector_tools("beta"),
},
AppInfo {
id: "alpha".to_string(),
@@ -1155,6 +1182,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
]
);
@@ -1183,6 +1211,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
let second_update = read_app_list_updated_notification(&mut mcp).await?;
assert_eq!(second_update.data, expected_final);
@@ -1219,6 +1248,7 @@ async fn experimental_feature_enablement_set_refreshes_apps_list_when_apps_turn_
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
let (server_url, server_handle, server_control) = start_apps_server_with_delays_and_control(
initial_connectors,
@@ -1267,6 +1297,7 @@ async fn experimental_feature_enablement_set_refreshes_apps_list_when_apps_turn_
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}]);
server_control.set_tools(vec![connector_tool("alpha", "Alpha App")?]);
@@ -1298,6 +1329,7 @@ async fn experimental_feature_enablement_set_refreshes_apps_list_when_apps_turn_
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: expected_connector_tools("alpha"),
}]
);
@@ -1515,6 +1547,17 @@ fn connector_tool(connector_id: &str, connector_name: &str) -> Result<Tool> {
Ok(tool)
}
fn expected_connector_tools(connector_id: &str) -> Vec<AppToolInfo> {
vec![AppToolInfo {
name: format!("connector_{connector_id}"),
description: Some("Connector test tool".to_string()),
input_schema: json!({
"additionalProperties": false,
"type": "object"
}),
}]
}
fn write_connectors_config(codex_home: &std::path::Path, base_url: &str) -> std::io::Result<()> {
let config_toml = codex_home.join("config.toml");
std::fs::write(

View File

@@ -364,6 +364,7 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
AppInfo {
id: "beta".to_string(),
@@ -379,6 +380,7 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
];
let tools = vec![connector_tool("beta", "Beta App")?];
@@ -461,6 +463,7 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
let (server_url, server_handle) = start_apps_server(connectors, Vec::new()).await?;

View File

@@ -279,6 +279,7 @@ async fn plugin_read_returns_app_needs_auth() -> Result<()> {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
AppInfo {
id: "beta".to_string(),
@@ -294,6 +295,7 @@ async fn plugin_read_returns_app_needs_auth() -> Result<()> {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
];
let tools = vec![connector_tool("beta", "Beta App")?];

View File

@@ -183,6 +183,7 @@ mod tests {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}
}
@@ -241,6 +242,7 @@ mod tests {
is_accessible,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}
}

View File

@@ -365,6 +365,7 @@ fn directory_app_to_app_info(app: DirectoryApp) -> AppInfo {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}
}

View File

@@ -38,6 +38,7 @@ mod tests {
is_accessible,
is_enabled,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}
}

View File

@@ -328,6 +328,7 @@ fn make_connector(id: &str, name: &str) -> AppInfo {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}
}

View File

@@ -14,6 +14,7 @@ use async_channel::unbounded;
pub use codex_app_server_protocol::AppBranding;
pub use codex_app_server_protocol::AppInfo;
pub use codex_app_server_protocol::AppMetadata;
pub use codex_app_server_protocol::AppToolInfo;
use codex_connectors::AllConnectorsCacheKey;
use codex_connectors::DirectoryListResponse;
use codex_login::token_data::TokenData;
@@ -524,6 +525,7 @@ pub(crate) fn accessible_connectors_from_mcp_tools(
normalize_connector_value(tool.connector_name.as_deref()),
normalize_connector_value(tool.connector_description.as_deref()),
tool.plugin_display_names.clone(),
app_tool_info_from_mcp_tool(tool),
))
});
collect_accessible_connectors(tools)
@@ -545,6 +547,7 @@ pub fn merge_connectors(
connector.is_accessible = true;
let connector_id = connector.id.clone();
if let Some(existing) = merged.get_mut(&connector_id) {
let connector_tools = std::mem::take(&mut connector.tools);
existing.is_accessible = true;
if existing.name == existing.id && connector.name != connector.id {
existing.name = connector.name;
@@ -561,6 +564,9 @@ pub fn merge_connectors(
if existing.distribution_channel.is_none() && connector.distribution_channel.is_some() {
existing.distribution_channel = connector.distribution_channel;
}
if !connector_tools.is_empty() {
existing.tools = connector_tools;
}
existing
.plugin_display_names
.extend(connector.plugin_display_names);
@@ -576,6 +582,9 @@ pub fn merge_connectors(
}
connector.plugin_display_names.sort_unstable();
connector.plugin_display_names.dedup();
connector
.tools
.sort_by(|left, right| left.name.cmp(&right.name));
}
merged.sort_by(|left, right| {
right
@@ -861,10 +870,20 @@ fn app_tool_policy_from_apps_config(
fn collect_accessible_connectors<I>(tools: I) -> Vec<AppInfo>
where
I: IntoIterator<Item = (String, Option<String>, Option<String>, Vec<String>)>,
I: IntoIterator<
Item = (
String,
Option<String>,
Option<String>,
Vec<String>,
AppToolInfo,
),
>,
{
let mut connectors: HashMap<String, (AppInfo, BTreeSet<String>)> = HashMap::new();
for (connector_id, connector_name, connector_description, plugin_display_names) in tools {
for (connector_id, connector_name, connector_description, plugin_display_names, app_tool) in
tools
{
let connector_name = connector_name.unwrap_or_else(|| connector_id.clone());
if let Some((existing, existing_plugin_display_names)) = connectors.get_mut(&connector_id) {
if existing.name == connector_id && connector_name != connector_id {
@@ -873,6 +892,7 @@ where
if existing.description.is_none() && connector_description.is_some() {
existing.description = connector_description;
}
existing.tools.push(app_tool);
existing_plugin_display_names.extend(plugin_display_names);
} else {
connectors.insert(
@@ -892,6 +912,7 @@ where
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: vec![app_tool],
},
plugin_display_names
.into_iter()
@@ -906,6 +927,9 @@ where
connector.plugin_display_names = plugin_display_names.into_iter().collect();
connector.install_url = Some(connector_install_url(&connector.name, &connector.id));
connector
.tools
.sort_by(|left, right| left.name.cmp(&right.name));
connector
})
.collect();
accessible.sort_by(|left, right| {
@@ -918,6 +942,14 @@ where
accessible
}
fn app_tool_info_from_mcp_tool(tool: &ToolInfo) -> AppToolInfo {
AppToolInfo {
name: tool.tool_name.clone(),
description: normalize_connector_value(tool.tool.description.as_deref()),
input_schema: serde_json::Value::Object(tool.tool.input_schema.as_ref().clone()),
}
}
fn plugin_app_to_app_info(connector_id: AppConnectorId) -> AppInfo {
// Leave the placeholder name as the connector id so merge_connectors() can
// replace it with canonical app metadata from directory fetches or
@@ -938,6 +970,7 @@ fn plugin_app_to_app_info(connector_id: AppConnectorId) -> AppInfo {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}
}

View File

@@ -49,6 +49,7 @@ fn app(id: &str) -> AppInfo {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}
}
@@ -79,6 +80,14 @@ fn test_tool_definition(tool_name: &str) -> Tool {
}
}
fn app_tool_info(tool_name: &str, description: Option<&str>) -> AppToolInfo {
AppToolInfo {
name: tool_name.to_string(),
description: description.map(ToOwned::to_owned),
input_schema: serde_json::Value::Object(JsonObject::default()),
}
}
fn google_calendar_accessible_connector(plugin_display_names: &[&str]) -> AppInfo {
AppInfo {
id: "calendar".to_string(),
@@ -94,6 +103,7 @@ fn google_calendar_accessible_connector(plugin_display_names: &[&str]) -> AppInf
is_accessible: true,
is_enabled: true,
plugin_display_names: plugin_names(plugin_display_names),
tools: Vec::new(),
}
}
@@ -158,6 +168,7 @@ fn merge_connectors_replaces_plugin_placeholder_name_with_accessible_name() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}]
);
assert_eq!(connector_mention_slug(&merged[0]), "google-calendar");
@@ -217,6 +228,10 @@ fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() {
is_accessible: true,
is_enabled: true,
plugin_display_names: plugin_names(&["beta", "sample"]),
tools: vec![
app_tool_info("calendar_create_event", None),
app_tool_info("calendar_list_events", None),
],
}]
);
}
@@ -273,6 +288,7 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal
is_accessible: true,
is_enabled: true,
plugin_display_names: plugin_names(&["calendar-plugin"]),
tools: vec![app_tool_info("calendar_list_events", None)],
}]
);
}
@@ -302,6 +318,7 @@ fn merge_connectors_unions_and_dedupes_plugin_display_names() {
is_accessible: true,
is_enabled: true,
plugin_display_names: plugin_names(&["alpha", "beta", "sample"]),
tools: Vec::new(),
}]
);
}
@@ -348,6 +365,10 @@ fn accessible_connectors_from_mcp_tools_preserves_description() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: vec![app_tool_info(
"calendar_create_event",
Some("Create a calendar event"),
)],
}]
);
}

View File

@@ -68,6 +68,7 @@ fn discoverable_connector(id: &str, name: &str, description: &str) -> Discoverab
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}))
}

View File

@@ -304,6 +304,7 @@ fn filter_tool_suggest_discoverable_tools_for_codex_tui_omits_plugins() {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
})),
DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo {
id: "slack@openai-curated".to_string(),
@@ -331,6 +332,7 @@ fn filter_tool_suggest_discoverable_tools_for_codex_tui_omits_plugins() {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}))]
);
}

View File

@@ -1803,6 +1803,7 @@ fn discoverable_connector(id: &str, name: &str, description: &str) -> Discoverab
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}))
}

View File

@@ -28,6 +28,7 @@ fn build_tool_suggestion_elicitation_request_uses_expected_shape() {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}));
let request = build_tool_suggestion_elicitation_request(
@@ -166,6 +167,7 @@ fn verified_connector_suggestion_completed_requires_accessible_connector() {
is_accessible: true,
is_enabled: false,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
assert!(verified_connector_suggestion_completed(
@@ -194,6 +196,7 @@ fn all_suggested_connectors_picked_up_requires_every_expected_connector() {
is_accessible: true,
is_enabled: false,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
assert!(all_suggested_connectors_picked_up(

View File

@@ -4726,6 +4726,7 @@ mod tests {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
composer.set_connector_mentions(Some(ConnectorsSnapshot { connectors }));
@@ -4768,6 +4769,7 @@ mod tests {
is_accessible: true,
is_enabled: false,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
composer.set_connector_mentions(Some(ConnectorsSnapshot { connectors }));
@@ -4868,6 +4870,7 @@ mod tests {
is_accessible: true,
is_enabled: true,
plugin_display_names: vec!["Google Calendar".to_string()],
tools: Vec::new(),
}],
}));
@@ -4964,6 +4967,7 @@ mod tests {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}],
}));
},
@@ -4998,6 +5002,7 @@ mod tests {
is_accessible: true,
is_enabled: false,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
composer.set_connector_mentions(Some(ConnectorsSnapshot { connectors }));

View File

@@ -525,6 +525,7 @@ async fn apps_popup_stays_loading_until_final_snapshot_updates() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}],
}),
/*is_final*/ false,
@@ -559,6 +560,7 @@ async fn apps_popup_stays_loading_until_final_snapshot_updates() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
codex_chatgpt::connectors::AppInfo {
id: linear_id.to_string(),
@@ -574,6 +576,7 @@ async fn apps_popup_stays_loading_until_final_snapshot_updates() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
],
}),
@@ -618,6 +621,7 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
codex_chatgpt::connectors::AppInfo {
id: linear_id.to_string(),
@@ -633,6 +637,7 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
];
chat.on_connectors_loaded(
@@ -658,6 +663,7 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}],
}),
/*is_final*/ false,
@@ -707,6 +713,7 @@ async fn apps_popup_preserves_selected_app_across_refresh() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
codex_chatgpt::connectors::AppInfo {
id: "slack".to_string(),
@@ -722,6 +729,7 @@ async fn apps_popup_preserves_selected_app_across_refresh() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
],
}),
@@ -753,6 +761,7 @@ async fn apps_popup_preserves_selected_app_across_refresh() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
codex_chatgpt::connectors::AppInfo {
id: "notion".to_string(),
@@ -768,6 +777,7 @@ async fn apps_popup_preserves_selected_app_across_refresh() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
codex_chatgpt::connectors::AppInfo {
id: "slack".to_string(),
@@ -783,6 +793,7 @@ async fn apps_popup_preserves_selected_app_across_refresh() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
],
}),
@@ -826,6 +837,7 @@ async fn apps_refresh_failure_with_cached_snapshot_triggers_pending_force_refetc
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}];
chat.connectors_cache = ConnectorsCacheState::Ready(ConnectorsSnapshot {
connectors: full_connectors.clone(),
@@ -869,6 +881,7 @@ async fn apps_popup_keeps_existing_full_snapshot_while_partial_refresh_loads() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
codex_chatgpt::connectors::AppInfo {
id: "unit_test_connector_2".to_string(),
@@ -884,6 +897,7 @@ async fn apps_popup_keeps_existing_full_snapshot_while_partial_refresh_loads() {
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
];
chat.on_connectors_loaded(
@@ -911,6 +925,7 @@ async fn apps_popup_keeps_existing_full_snapshot_while_partial_refresh_loads() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
codex_chatgpt::connectors::AppInfo {
id: "connector_openai_hidden".to_string(),
@@ -926,6 +941,7 @@ async fn apps_popup_keeps_existing_full_snapshot_while_partial_refresh_loads() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
},
],
}),
@@ -974,6 +990,7 @@ async fn apps_refresh_failure_without_full_snapshot_falls_back_to_installed_apps
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}],
}),
/*is_final*/ false,
@@ -1033,6 +1050,7 @@ async fn apps_popup_shows_disabled_status_for_installed_but_disabled_apps() {
is_accessible: true,
is_enabled: false,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}],
}),
/*is_final*/ true,
@@ -1087,6 +1105,7 @@ async fn apps_initial_load_applies_enabled_state_from_config() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}],
}),
/*is_final*/ true,
@@ -1153,6 +1172,7 @@ async fn apps_initial_load_applies_enabled_state_from_requirements_with_user_ove
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}],
}),
/*is_final*/ true,
@@ -1217,6 +1237,7 @@ async fn apps_initial_load_applies_enabled_state_from_requirements_without_user_
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}],
}),
/*is_final*/ true,
@@ -1266,6 +1287,7 @@ async fn apps_refresh_preserves_toggled_enabled_state() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}],
}),
/*is_final*/ true,
@@ -1288,6 +1310,7 @@ async fn apps_refresh_preserves_toggled_enabled_state() {
is_accessible: true,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}],
}),
/*is_final*/ true,
@@ -1337,6 +1360,7 @@ async fn apps_popup_for_not_installed_app_uses_install_only_selected_description
is_accessible: false,
is_enabled: true,
plugin_display_names: Vec::new(),
tools: Vec::new(),
}],
}),
/*is_final*/ true,