mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Enrich app/read connector metadata (#34417)
## What changed - Add `iconUrlDark`, `distributionChannel`, `installUrl`, and `pluginDisplayNames` to the experimental `app/read` connector metadata. - Populate plugin display names from enabled plugins without starting their MCP servers. - Accept both dark-icon field spellings from the batch API and keep the new upstream fields optional for compatibility with older responses. ## Testing - Cover the serialized response fields, legacy batch responses, and plugin display-name lookup without MCP startup. GitOrigin-RevId: 68601a104de839c0f4f4621d33b0e4d08ad88c64
This commit is contained in:
+25
@@ -8890,18 +8890,43 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"distributionChannel": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"iconUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"iconUrlDark": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"installUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginDisplayNames": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"toolSummaries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AppToolSummary"
|
||||
|
||||
+25
@@ -5068,18 +5068,43 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"distributionChannel": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"iconUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"iconUrlDark": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"installUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginDisplayNames": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"toolSummaries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AppToolSummary"
|
||||
|
||||
@@ -32,18 +32,43 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"distributionChannel": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"iconUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"iconUrlDark": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"installUrl": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginDisplayNames": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"toolSummaries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AppToolSummary"
|
||||
|
||||
@@ -6,4 +6,4 @@ import type { AppToolSummary } from "./AppToolSummary";
|
||||
/**
|
||||
* EXPERIMENTAL - metadata returned by app/read.
|
||||
*/
|
||||
export type ConnectorMetadata = { id: string, name: string, description: string | null, iconUrl: string | null, toolSummaries: Array<AppToolSummary> | null, };
|
||||
export type ConnectorMetadata = { id: string, name: string, description: string | null, iconUrl: string | null, iconUrlDark: string | null, distributionChannel: string | null, installUrl: string | null, pluginDisplayNames: Array<string>, toolSummaries: Array<AppToolSummary> | null, };
|
||||
|
||||
@@ -202,6 +202,11 @@ pub struct ConnectorMetadata {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub icon_url: Option<String>,
|
||||
pub icon_url_dark: Option<String>,
|
||||
pub distribution_channel: Option<String>,
|
||||
pub install_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub plugin_display_names: Vec<String>,
|
||||
pub tool_summaries: Option<Vec<AppToolSummary>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_connectors::AppReview;
|
||||
use codex_connectors::AppScreenshot;
|
||||
use codex_connectors::ConnectorMetadata;
|
||||
use codex_connectors::ConnectorToolSummary;
|
||||
use codex_connectors::metadata::connector_install_url;
|
||||
|
||||
/// Converts connector-domain app metadata owned by `codex-connectors` into the app-server wire
|
||||
/// type owned by `codex-app-server-protocol`.
|
||||
@@ -59,20 +60,27 @@ pub(crate) fn app_info_to_api(app: AppInfo) -> ApiAppInfo {
|
||||
/// Converts metadata-only connector data into the app-server wire type.
|
||||
///
|
||||
/// Keeping this separate from app_info_to_api makes it impossible for app/read to accidentally
|
||||
/// grow runtime state from the broader app/list shape.
|
||||
/// expose full runtime tool state from the broader app/list path.
|
||||
pub(crate) fn connector_metadata_to_api(metadata: ConnectorMetadata) -> ApiConnectorMetadata {
|
||||
let ConnectorMetadata {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
icon_url,
|
||||
icon_url_dark,
|
||||
distribution_channel,
|
||||
tool_summaries,
|
||||
} = metadata;
|
||||
let install_url = Some(connector_install_url(&name, &id));
|
||||
ApiConnectorMetadata {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
icon_url,
|
||||
icon_url_dark,
|
||||
distribution_channel,
|
||||
install_url,
|
||||
plugin_display_names: Vec::new(),
|
||||
tool_summaries: tool_summaries.map(|tools| {
|
||||
tools
|
||||
.into_iter()
|
||||
|
||||
@@ -81,9 +81,28 @@ impl AppsRequestProcessor {
|
||||
} = connectors::read_connector_metadata(&config, auth, &app_ids, include_tools)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to read app metadata: {err}")))?;
|
||||
let loaded_plugins = self
|
||||
.thread_manager
|
||||
.plugins_manager()
|
||||
.plugins_for_config(&config.plugins_config_input())
|
||||
.await;
|
||||
let connector_snapshot =
|
||||
codex_connectors::ConnectorSnapshot::from_plugin_capability_summaries(
|
||||
loaded_plugins.capability_summaries(),
|
||||
);
|
||||
let apps = apps
|
||||
.into_iter()
|
||||
.map(|metadata| {
|
||||
let mut app = connector_metadata_to_api(metadata);
|
||||
app.plugin_display_names = connector_snapshot
|
||||
.plugin_display_names_for_connector_id(app.id.as_str())
|
||||
.to_vec();
|
||||
app
|
||||
})
|
||||
.collect();
|
||||
Ok(Some(
|
||||
AppsReadResponse {
|
||||
apps: apps.into_iter().map(connector_metadata_to_api).collect(),
|
||||
apps,
|
||||
missing_app_ids,
|
||||
}
|
||||
.into(),
|
||||
|
||||
@@ -16,8 +16,8 @@ use axum::extract::State;
|
||||
use axum::http::HeaderMap;
|
||||
use axum::http::StatusCode;
|
||||
use axum::http::header::AUTHORIZATION;
|
||||
use axum::routing::any;
|
||||
use axum::routing::post;
|
||||
use codex_app_server_protocol::AppToolSummary;
|
||||
use codex_app_server_protocol::AppsReadParams;
|
||||
use codex_app_server_protocol::AppsReadResponse;
|
||||
use codex_app_server_protocol::ConnectorMetadata;
|
||||
@@ -44,11 +44,22 @@ async fn app_read_deduplicates_orders_partial_misses_and_reuses_cached_metadata(
|
||||
.plan_type("plus")
|
||||
.chatgpt_account_id("account-123"),
|
||||
)?;
|
||||
let mut beta_response = app_response(
|
||||
"beta",
|
||||
"Beta",
|
||||
Some("https://files.openai.com/content?id=beta"),
|
||||
);
|
||||
let beta_icon_dark_url = beta_response
|
||||
.as_object_mut()
|
||||
.expect("app response is an object")
|
||||
.remove("icon_dark_url")
|
||||
.expect("app response contains icon_dark_url");
|
||||
beta_response["icon_url_dark"] = beta_icon_dark_url;
|
||||
let state = BatchServerState::new(
|
||||
json!({
|
||||
"apps": [
|
||||
app_response("alpha", "Alpha", Some("https://files.openai.com/content?id=alpha")),
|
||||
app_response("beta", "Beta", Some("https://files.openai.com/content?id=beta")),
|
||||
beta_response,
|
||||
]
|
||||
}),
|
||||
&access_token,
|
||||
@@ -81,12 +92,23 @@ async fn app_read_deduplicates_orders_partial_misses_and_reuses_cached_metadata(
|
||||
LoginAccountResponse::ChatgptAuthTokens {}
|
||||
);
|
||||
|
||||
let response = read_apps(
|
||||
let raw_response = read_apps_raw(
|
||||
&mut mcp,
|
||||
vec!["beta", "missing", "alpha", "beta", "forbidden"],
|
||||
/*include_tools*/ true,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
raw_response,
|
||||
json!({
|
||||
"apps": [
|
||||
metadata_json("beta", "Beta", Some("https://files.openai.com/content?id=beta")),
|
||||
metadata_json("alpha", "Alpha", Some("https://files.openai.com/content?id=alpha")),
|
||||
],
|
||||
"missingAppIds": ["missing", "forbidden"],
|
||||
})
|
||||
);
|
||||
let response: AppsReadResponse = serde_json::from_value(raw_response)?;
|
||||
assert_eq!(
|
||||
response,
|
||||
AppsReadResponse {
|
||||
@@ -290,6 +312,89 @@ async fn app_read_backend_failure_preserves_fresh_cached_records() -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_read_adds_plugin_display_names_without_starting_mcp() -> Result<()> {
|
||||
let state = BatchServerState::new(
|
||||
json!({
|
||||
"apps": [
|
||||
app_response("alpha", "Alpha", /*icon_url*/ None),
|
||||
app_response("unclaimed", "Unclaimed", /*icon_url*/ None),
|
||||
]
|
||||
}),
|
||||
"chatgpt-token",
|
||||
"codex",
|
||||
);
|
||||
let (server_url, server_handle) = start_batch_server(state.clone()).await?;
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
chatgpt_base_url = "{server_url}"
|
||||
|
||||
[features]
|
||||
connectors = true
|
||||
plugins = true
|
||||
|
||||
[plugins."alpha-z@test"]
|
||||
enabled = true
|
||||
|
||||
[plugins."alpha-a@test"]
|
||||
enabled = true
|
||||
|
||||
[plugins."disabled@test"]
|
||||
enabled = false
|
||||
"#,
|
||||
),
|
||||
)?;
|
||||
write_plugin_app(codex_home.path(), "alpha-z", "Alpha Z", "alpha")?;
|
||||
write_plugin_app(codex_home.path(), "alpha-a", "Alpha A", "alpha")?;
|
||||
write_plugin_app(
|
||||
codex_home.path(),
|
||||
"disabled",
|
||||
"Disabled Plugin",
|
||||
"unclaimed",
|
||||
)?;
|
||||
write_auth(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let response = read_apps(
|
||||
&mut mcp,
|
||||
vec!["alpha", "unclaimed"],
|
||||
/*include_tools*/ false,
|
||||
)
|
||||
.await?;
|
||||
let mut alpha = metadata_without_tools("alpha", "Alpha", /*icon_url*/ None);
|
||||
alpha.plugin_display_names = vec!["Alpha A".to_string(), "Alpha Z".to_string()];
|
||||
assert_eq!(
|
||||
response,
|
||||
AppsReadResponse {
|
||||
apps: vec![
|
||||
alpha,
|
||||
metadata_without_tools("unclaimed", "Unclaimed", /*icon_url*/ None),
|
||||
],
|
||||
missing_app_ids: Vec::new(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
state.requests(),
|
||||
vec![json!({
|
||||
"app_ids": ["alpha", "unclaimed"],
|
||||
"include_tools": false,
|
||||
})]
|
||||
);
|
||||
assert_eq!(state.mcp_requests(), 0);
|
||||
|
||||
server_handle.abort();
|
||||
let _ = server_handle.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_read_rejects_more_than_one_hundred_input_ids() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -321,6 +426,16 @@ async fn read_apps(
|
||||
app_ids: Vec<&str>,
|
||||
include_tools: bool,
|
||||
) -> Result<AppsReadResponse> {
|
||||
Ok(serde_json::from_value(
|
||||
read_apps_raw(mcp, app_ids, include_tools).await?,
|
||||
)?)
|
||||
}
|
||||
|
||||
async fn read_apps_raw(
|
||||
mcp: &mut TestAppServer,
|
||||
app_ids: Vec<&str>,
|
||||
include_tools: bool,
|
||||
) -> Result<Value> {
|
||||
let request_id = mcp
|
||||
.send_apps_read_request(AppsReadParams {
|
||||
app_ids: app_ids.into_iter().map(str::to_string).collect(),
|
||||
@@ -332,21 +447,29 @@ async fn read_apps(
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
to_response(response)
|
||||
Ok(response.result)
|
||||
}
|
||||
|
||||
fn metadata(id: &str, name: &str, icon_url: Option<&str>) -> ConnectorMetadata {
|
||||
ConnectorMetadata {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
description: Some(format!("{name} description")),
|
||||
icon_url: icon_url.map(str::to_string),
|
||||
tool_summaries: Some(vec![AppToolSummary {
|
||||
name: format!("{id}_tool"),
|
||||
title: Some(format!("{name} Tool")),
|
||||
description: format!("Use {name}"),
|
||||
}]),
|
||||
}
|
||||
serde_json::from_value(metadata_json(id, name, icon_url)).expect("valid app metadata JSON")
|
||||
}
|
||||
|
||||
fn metadata_json(id: &str, name: &str, icon_url: Option<&str>) -> Value {
|
||||
json!({
|
||||
"id": id,
|
||||
"name": name,
|
||||
"description": format!("{name} description"),
|
||||
"iconUrl": icon_url,
|
||||
"iconUrlDark": format!("https://files.openai.com/content?id={id}-dark"),
|
||||
"distributionChannel": "ECOSYSTEM_DIRECTORY",
|
||||
"installUrl": format!("https://chatgpt.com/apps/{}/{id}", name.to_ascii_lowercase()),
|
||||
"pluginDisplayNames": [],
|
||||
"toolSummaries": [{
|
||||
"name": format!("{id}_tool"),
|
||||
"title": format!("{name} Tool"),
|
||||
"description": format!("Use {name}"),
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
fn metadata_without_tools(id: &str, name: &str, icon_url: Option<&str>) -> ConnectorMetadata {
|
||||
@@ -362,12 +485,13 @@ fn app_response(id: &str, name: &str, icon_url: Option<&str>) -> Value {
|
||||
"name": name,
|
||||
"description": format!("{name} description"),
|
||||
"icon_url": null,
|
||||
"icon_dark_url": format!("https://files.openai.com/content?id={id}-dark"),
|
||||
"distribution_channel": "ECOSYSTEM_DIRECTORY",
|
||||
"tools": [{
|
||||
"name": format!("{id}_tool"),
|
||||
"title": format!("{name} Tool"),
|
||||
"description": format!("Use {name}"),
|
||||
}],
|
||||
"distribution_channel": "ECOSYSTEM_DIRECTORY",
|
||||
"branding": {
|
||||
"category": "PRODUCTIVITY",
|
||||
"developer": "Test Developer",
|
||||
@@ -391,7 +515,7 @@ fn app_response(id: &str, name: &str, icon_url: Option<&str>) -> Value {
|
||||
"version": "1.0.0",
|
||||
"version_id": "version-1",
|
||||
"version_notes": "Initial release",
|
||||
"first_party_type": "test",
|
||||
"first_party_type": "must-not-escape",
|
||||
"first_party_requires_install": true,
|
||||
"show_in_composer_when_unlinked": true,
|
||||
"subtitle": "must-not-escape",
|
||||
@@ -408,6 +532,33 @@ fn app_response(id: &str, name: &str, icon_url: Option<&str>) -> Value {
|
||||
response
|
||||
}
|
||||
|
||||
fn write_plugin_app(
|
||||
codex_home: &Path,
|
||||
plugin_name: &str,
|
||||
display_name: &str,
|
||||
connector_id: &str,
|
||||
) -> Result<()> {
|
||||
let plugin_root = codex_home
|
||||
.join("plugins/cache/test")
|
||||
.join(plugin_name)
|
||||
.join("local");
|
||||
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
|
||||
std::fs::write(
|
||||
plugin_root.join(".codex-plugin/plugin.json"),
|
||||
serde_json::to_vec(&json!({
|
||||
"name": plugin_name,
|
||||
"interface": { "displayName": display_name },
|
||||
}))?,
|
||||
)?;
|
||||
std::fs::write(
|
||||
plugin_root.join(".app.json"),
|
||||
serde_json::to_vec(&json!({
|
||||
"apps": { "app": { "id": connector_id } }
|
||||
}))?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_apps_config(
|
||||
codex_home: &Path,
|
||||
base_url: &str,
|
||||
@@ -445,6 +596,7 @@ fn write_auth(codex_home: &Path) -> Result<()> {
|
||||
#[derive(Clone)]
|
||||
struct BatchServerState {
|
||||
requests: Arc<StdMutex<Vec<Value>>>,
|
||||
mcp_requests: Arc<StdMutex<usize>>,
|
||||
response: Arc<StdMutex<Value>>,
|
||||
status: Arc<StdMutex<StatusCode>>,
|
||||
access_token: String,
|
||||
@@ -455,6 +607,7 @@ impl BatchServerState {
|
||||
fn new(response: Value, access_token: &str, expected_product_sku: &str) -> Self {
|
||||
Self {
|
||||
requests: Arc::new(StdMutex::new(Vec::new())),
|
||||
mcp_requests: Arc::new(StdMutex::new(0)),
|
||||
response: Arc::new(StdMutex::new(response)),
|
||||
status: Arc::new(StdMutex::new(StatusCode::OK)),
|
||||
access_token: access_token.to_string(),
|
||||
@@ -475,6 +628,13 @@ impl BatchServerState {
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = status;
|
||||
}
|
||||
|
||||
fn mcp_requests(&self) -> usize {
|
||||
*self
|
||||
.mcp_requests
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_batch_server(state: BatchServerState) -> Result<(String, JoinHandle<()>)> {
|
||||
@@ -482,6 +642,7 @@ async fn start_batch_server(state: BatchServerState) -> Result<(String, JoinHand
|
||||
let addr = listener.local_addr()?;
|
||||
let router = Router::new()
|
||||
.route("/ps/apps/batch", post(batch_apps))
|
||||
.route("/api/codex/ps/mcp", any(unexpected_mcp_request))
|
||||
.with_state(state);
|
||||
let handle = tokio::spawn(async move {
|
||||
let _ = axum::serve(listener, router).await;
|
||||
@@ -489,6 +650,14 @@ async fn start_batch_server(state: BatchServerState) -> Result<(String, JoinHand
|
||||
Ok((format!("http://{addr}"), handle))
|
||||
}
|
||||
|
||||
async fn unexpected_mcp_request(State(state): State<BatchServerState>) -> StatusCode {
|
||||
*state
|
||||
.mcp_requests
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) += 1;
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
|
||||
async fn batch_apps(
|
||||
State(state): State<BatchServerState>,
|
||||
headers: HeaderMap,
|
||||
|
||||
@@ -233,6 +233,10 @@ struct BatchApp {
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
icon_url: Option<String>,
|
||||
#[serde(default, rename = "icon_dark_url", alias = "icon_url_dark")]
|
||||
icon_url_dark: Option<String>,
|
||||
#[serde(default)]
|
||||
distribution_channel: Option<String>,
|
||||
#[serde(default)]
|
||||
tools: Option<Vec<BatchAppToolSummary>>,
|
||||
}
|
||||
@@ -250,6 +254,8 @@ fn batch_app_to_metadata(app: BatchApp) -> ConnectorMetadata {
|
||||
name,
|
||||
description,
|
||||
icon_url,
|
||||
icon_url_dark,
|
||||
distribution_channel,
|
||||
tools,
|
||||
} = app;
|
||||
ConnectorMetadata {
|
||||
@@ -257,6 +263,8 @@ fn batch_app_to_metadata(app: BatchApp) -> ConnectorMetadata {
|
||||
name,
|
||||
description,
|
||||
icon_url,
|
||||
icon_url_dark,
|
||||
distribution_channel,
|
||||
tool_summaries: tools.map(|tools| {
|
||||
tools
|
||||
.into_iter()
|
||||
@@ -351,6 +359,32 @@ mod tests {
|
||||
use codex_connectors::metadata::connector_install_url;
|
||||
use codex_plugin::AppConnectorId;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn batch_app_accepts_missing_optional_metadata() {
|
||||
let app = serde_json::from_value::<BatchApp>(json!({
|
||||
"id": "alpha",
|
||||
"name": "Alpha",
|
||||
"description": "Alpha description",
|
||||
"icon_url": null,
|
||||
"tools": null,
|
||||
}))
|
||||
.expect("valid legacy batch app");
|
||||
|
||||
assert_eq!(
|
||||
batch_app_to_metadata(app),
|
||||
ConnectorMetadata {
|
||||
id: "alpha".to_string(),
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha description".to_string()),
|
||||
icon_url: None,
|
||||
icon_url_dark: None,
|
||||
distribution_channel: None,
|
||||
tool_summaries: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn app(id: &str) -> AppInfo {
|
||||
AppInfo {
|
||||
|
||||
@@ -16,14 +16,16 @@ pub struct ConnectorToolSummary {
|
||||
/// Metadata returned by the app batch-read API.
|
||||
///
|
||||
/// This intentionally excludes connector runtime state, full actions, and model descriptions.
|
||||
/// Tool summaries contain display text only, and the icon URL is already projected as a public
|
||||
/// URL by the backend.
|
||||
/// Tool summaries contain display text only, and icon URLs are already projected as public URLs by
|
||||
/// the backend.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ConnectorMetadata {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub icon_url: Option<String>,
|
||||
pub icon_url_dark: Option<String>,
|
||||
pub distribution_channel: Option<String>,
|
||||
pub tool_summaries: Option<Vec<ConnectorToolSummary>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ fn metadata(id: &str) -> ConnectorMetadata {
|
||||
name: format!("{id} name"),
|
||||
description: None,
|
||||
icon_url: None,
|
||||
icon_url_dark: None,
|
||||
distribution_channel: None,
|
||||
tool_summaries: None,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user