Track connectors detected in external agent sessions (#36959)

## What changed

- Return connector candidates found in detected external agent sessions, aggregating session counts by connector name.
- Resolve connector names from both cached plugin manifests and project MCP server metadata.
- Persist detected connector clues in the session import ledger and merge them into completed imports without case-insensitive duplicates.

## Testing

- Add coverage for project MCP metadata detection and for accumulating detected and imported connector candidates.

GitOrigin-RevId: 97a7dfe4a61599686b2222cddd9595acec8b4501
This commit is contained in:
charlesgong-openai authored and copyberry committed 2026-08-04 19:45:56 +00:00
1 parent 4bd5b9fd09
commit 92689b6b7b
8 files changed
+399 -19

No files matched your search

@@ -14,6 +14,9 @@ use crate::sessions::ExternalAgentSessionMigration;
const PLUGIN_CACHE_DIR: &str = "plugins/cache";
const PLUGIN_MANIFEST_PATH: &str = ".cursor-plugin/plugin.json";
const MCP_CONFIG_PATH: &str = ".mcp.json";
const PROJECT_MCP_DIR: &str = "mcps";
const PROJECT_MCP_SERVER_METADATA_PATH: &str = "SERVER_METADATA.json";
const AGENT_TRANSCRIPTS_DIR: &str = "agent-transcripts";
const MCP_TOOL_CALL: &str = "CallMcpTool";
#[derive(Deserialize)]
@@ -24,17 +27,23 @@ struct PluginManifest {
mcp_servers: Option<JsonValue>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ProjectMcpServerMetadata {
server_identifier: String,
server_name: String,
}
pub(crate) fn detect_cur_session_connectors(
sessions: &[ExternalAgentSessionMigration],
external_agent_home: &Path,
) -> Vec<DetectedConnectorCandidate> {
let connector_names_by_server_id = cached_connector_names_by_server_id(external_agent_home);
if connector_names_by_server_id.is_empty() {
return Vec::new();
}
let cached_connector_names_by_server_id =
cached_connector_names_by_server_id(external_agent_home);
let mut candidates = BTreeMap::<String, DetectedConnectorCandidate>::new();
for session in sessions {
let mut connector_names_by_server_id = cached_connector_names_by_server_id.clone();
connector_names_by_server_id.extend(project_connector_names_by_server_id(session));
for (key, name) in session_connector_names(session, &connector_names_by_server_id) {
let candidate = candidates.entry(key).or_insert(DetectedConnectorCandidate {
name,
@@ -47,6 +56,36 @@ pub(crate) fn detect_cur_session_connectors(
candidates.into_values().collect()
}
fn project_connector_names_by_server_id(
session: &ExternalAgentSessionMigration,
) -> BTreeMap<String, String> {
let Some(project_root) = session
.path
.ancestors()
.find(|path| path.file_name().and_then(|name| name.to_str()) == Some(AGENT_TRANSCRIPTS_DIR))
.and_then(Path::parent)
else {
return BTreeMap::new();
};
let mut connector_names = BTreeMap::new();
for server_root in child_directories(&project_root.join(PROJECT_MCP_DIR)) {
let Some(metadata) = read_project_mcp_server_metadata(&server_root) else {
continue;
};
let Some(display_name) =
crate::sessions::normalized_connector_display_name(Some(&metadata.server_name))
else {
continue;
};
let server_identifier = metadata.server_identifier.trim();
if server_identifier.is_empty() {
continue;
}
connector_names.insert(server_identifier.to_lowercase(), display_name);
}
connector_names
}
fn session_connector_names(
session: &ExternalAgentSessionMigration,
connector_names_by_server_id: &BTreeMap<String, String>,
@@ -134,6 +173,11 @@ fn read_plugin_manifest(version_root: &Path) -> Option<PluginManifest> {
serde_json::from_str(&contents).ok()
}
fn read_project_mcp_server_metadata(server_root: &Path) -> Option<ProjectMcpServerMetadata> {
let contents = fs::read_to_string(server_root.join(PROJECT_MCP_SERVER_METADATA_PATH)).ok()?;
serde_json::from_str(&contents).ok()
}
fn cached_mcp_server_names(version_root: &Path, manifest: &PluginManifest) -> Vec<String> {
manifest
.mcp_servers
@@ -83,6 +83,61 @@ fn detects_connector_used_by_session_mcp_tool_call() {
);
}
#[test]
fn detects_connector_from_project_mcp_server_metadata() {
let source_home = TempDir::new().expect("source home");
let project_root = source_home.path().join("projects/project");
let session_path = project_root
.join(AGENT_TRANSCRIPTS_DIR)
.join("session-1")
.join("session-1.jsonl");
fs::create_dir_all(session_path.parent().expect("session parent")).expect("session directory");
fs::write(
&session_path,
serde_json::json!({
"role": "assistant",
"message": {
"content": [{
"type": "tool_use",
"name": "CallMcpTool",
"input": {"server": "plugin-figma-figma", "toolName": "whoami"}
}]
}
})
.to_string(),
)
.expect("session");
let server_root = project_root
.join(PROJECT_MCP_DIR)
.join("plugin-figma-figma");
fs::create_dir_all(&server_root).expect("server directory");
fs::write(
server_root.join(PROJECT_MCP_SERVER_METADATA_PATH),
serde_json::json!({
"serverIdentifier": "plugin-figma-figma",
"serverName": "Figma",
})
.to_string(),
)
.expect("server metadata");
assert_eq!(
detect_cur_session_connectors(
&[ExternalAgentSessionMigration {
path: session_path,
cwd: source_home.path().to_path_buf(),
title: None,
}],
source_home.path(),
),
vec![DetectedConnectorCandidate {
name: "Figma".to_string(),
session_count: 1,
source: DetectedConnectorSource::SessionToolUse,
}]
);
}
fn write_cached_plugin(
source_home: &Path,
plugin_name: &str,