Skip to content

Commit a02ee6d

Browse files
committed
feat: add universal cross-root MCP support
Introduce root-scoped runtime state and extend the `project_root` contract across the `rpg-mcp` tool surface so calls can target an effective root without mutating the active session root. `set_project_root` remains the explicit mechanism for switching the default root for later calls. This Stage 1 functionality commit folds the validated Rust changes into one cross-root checkpoint. It includes the root-aware server and tool plumbing, stdio smoke coverage for the expanded tool surface, startup deadlock prevention, blank-parameter and empty-batch hardening needed for real MCP clients, the large-repository sharded hierarchy deadlock fix, and the shared embedding model cache needed for stable cross-root semantic search behavior. Live validation confirmed the resulting surface on healthy external roots, including search, fetch, explore, build, reload, update, lifting submission, routing submission, file synthesis submission, hierarchy submission, and large-root hierarchy workflows. Documentation is kept separate from this Stage 1 functionality commit.
1 parent 26b24ee commit a02ee6d

5 files changed

Lines changed: 1486 additions & 602 deletions

File tree

crates/rpg-mcp/src/main.rs

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@ async fn main() -> Result<()> {
5151

5252
// Auto-update graph on startup if stale (structural-only, no LLM)
5353
{
54-
let mut lock = server.graph.write().await;
55-
if let Some(ref mut graph) = *lock
54+
let project_root = server.project_root().await;
55+
let root_state = server.root_state(&project_root).await;
56+
let mut root_state = root_state.write().await;
57+
if let Some(ref mut graph) = root_state.graph
5658
&& let (Some(base), Ok(head)) = (
5759
&graph.base_commit.clone(),
58-
rpg_encoder::evolution::get_head_sha(&server.project_root().await),
60+
rpg_encoder::evolution::get_head_sha(&project_root),
5961
)
6062
{
6163
if *base != head {
@@ -71,7 +73,7 @@ async fn main() -> Result<()> {
7173
let qcache_result =
7274
rpg_parser::paradigms::query_engine::QueryCache::compile_all(&paradigm_defs);
7375
let active_defs = rpg_parser::paradigms::detect_paradigms_toml(
74-
&server.project_root().await,
76+
&project_root,
7577
&detected_langs,
7678
&paradigm_defs,
7779
);
@@ -86,27 +88,21 @@ async fn main() -> Result<()> {
8688
});
8789
match rpg_encoder::evolution::run_update(
8890
graph,
89-
&server.project_root().await,
91+
&project_root,
9092
None,
9193
pipeline.as_ref(),
9294
) {
9395
Ok(s) => {
9496
graph.metadata.paradigms = paradigm_names;
95-
let _ = storage::save(&server.project_root().await, graph);
96-
// Persist stale entity IDs from the startup sync so
97-
// lifting_status sees them on the first query. Every
98-
// other path that produces a summary feeds
99-
// `modified_entity_ids` into `stale_entity_ids`
100-
// (`auto_sync_if_stale`, `update_rpg`). The startup
101-
// path is the one exception — without this, modified
102-
// entities from between the last lift and this startup
103-
// are silently dropped across the session boundary.
97+
let _ = storage::save(&project_root, graph);
98+
let existing_ids: std::collections::HashSet<String> =
99+
graph.entities.keys().cloned().collect();
104100
{
105-
let mut stale = server.stale_entity_ids.write().await;
101+
let stale = &mut root_state.stale_entity_ids;
106102
for id in &s.modified_entity_ids {
107103
stale.insert(id.clone());
108104
}
109-
stale.retain(|id| graph.entities.contains_key(id));
105+
stale.retain(|id| existing_ids.contains(id));
110106
}
111107
eprintln!(
112108
" Auto-update complete: +{} -{} ~{}",
@@ -123,12 +119,15 @@ async fn main() -> Result<()> {
123119
// changeset) match instead of redundantly re-running the
124120
// workdir diff. Must use the real empty-workdir changeset
125121
// hash (not an empty string) for the match to fire.
126-
let project_root = server.project_root().await;
127-
*server.last_auto_sync_head.write().await =
122+
root_state.last_auto_sync_head =
128123
rpg_encoder::evolution::get_head_sha(&project_root).ok();
129-
*server.last_auto_sync_changeset.write().await =
124+
root_state.last_auto_sync_changeset =
130125
Some(RpgServer::compute_changeset_hash(&[], &project_root));
131-
*server.last_auto_sync_workdir_paths.write().await = std::collections::HashSet::new();
126+
root_state.last_auto_sync_workdir_paths = std::collections::HashSet::new();
127+
drop(root_state);
128+
server
129+
.sync_default_root_compat_from_state(&project_root)
130+
.await;
132131
}
133132
}
134133

crates/rpg-mcp/src/params.rs

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use serde::Deserialize;
88
pub(crate) struct SearchNodeParams {
99
/// The search query describing what you're looking for
1010
pub(crate) query: String,
11+
/// Optional project root override for this call only.
12+
pub(crate) project_root: Option<String>,
1113
/// Search mode: 'features', 'snippets', or 'auto' (default: 'auto')
1214
pub(crate) mode: Option<String>,
1315
/// Optional hierarchy scope to restrict search (e.g., 'Security/auth'). Comma-separated for multiple scopes.
@@ -27,6 +29,8 @@ pub(crate) struct SearchNodeParams {
2729
pub(crate) struct FetchNodeParams {
2830
/// The entity ID to fetch (e.g., 'src/auth.rs:validate_token')
2931
pub(crate) entity_id: String,
32+
/// Optional project root override for this call only.
33+
pub(crate) project_root: Option<String>,
3034
/// Multiple entity IDs to fetch in batch (overrides entity_id when provided)
3135
pub(crate) entity_ids: Option<Vec<String>>,
3236
/// Comma-separated fields to include: "features", "source", "deps", "hierarchy". Omit for all fields.
@@ -40,6 +44,8 @@ pub(crate) struct FetchNodeParams {
4044
pub(crate) struct ExploreRpgParams {
4145
/// The entity ID to start exploration from
4246
pub(crate) entity_id: String,
47+
/// Optional project root override for this call only.
48+
pub(crate) project_root: Option<String>,
4349
/// Multiple entity IDs to explore from in batch (overrides entity_id when provided)
4450
pub(crate) entity_ids: Option<Vec<String>>,
4551
/// Traversal direction: 'upstream', 'downstream', or 'both'
@@ -56,6 +62,12 @@ pub(crate) struct ExploreRpgParams {
5662
pub(crate) max_results: Option<usize>,
5763
}
5864

65+
#[derive(Debug, Default, Deserialize, JsonSchema)]
66+
pub(crate) struct RpgInfoParams {
67+
/// Optional project root override for this call only.
68+
pub(crate) project_root: Option<String>,
69+
}
70+
5971
/// Parameters for the `set_project_root` tool.
6072
#[derive(Debug, Deserialize, JsonSchema)]
6173
pub(crate) struct SetProjectRootParams {
@@ -66,6 +78,8 @@ pub(crate) struct SetProjectRootParams {
6678
/// Parameters for the `build_rpg` tool.
6779
#[derive(Debug, Deserialize, JsonSchema)]
6880
pub(crate) struct BuildRpgParams {
81+
/// Optional project root override for this call only.
82+
pub(crate) project_root: Option<String>,
6983
/// Primary language override (auto-detected if not specified)
7084
pub(crate) language: Option<String>,
7185
/// Glob pattern to include files (e.g., "src/**/*.rs")
@@ -77,22 +91,42 @@ pub(crate) struct BuildRpgParams {
7791
/// Parameters for the `update_rpg` tool.
7892
#[derive(Debug, Deserialize, JsonSchema)]
7993
pub(crate) struct UpdateRpgParams {
94+
/// Optional project root override for this call only.
95+
pub(crate) project_root: Option<String>,
8096
/// Base commit SHA to diff from (defaults to RPG's stored base_commit)
8197
pub(crate) since: Option<String>,
8298
}
8399

100+
/// Parameters for the `reload_rpg` tool.
101+
#[derive(Debug, Default, Deserialize, JsonSchema)]
102+
pub(crate) struct ReloadRpgParams {
103+
/// Optional project root override for this call only.
104+
pub(crate) project_root: Option<String>,
105+
}
106+
107+
/// Parameters for the `lifting_status` tool.
108+
#[derive(Debug, Default, Deserialize, JsonSchema)]
109+
pub(crate) struct LiftingStatusParams {
110+
/// Optional project root override for this call only.
111+
pub(crate) project_root: Option<String>,
112+
}
113+
84114
/// Parameters for the `get_entities_for_lifting` tool.
85115
#[derive(Debug, Deserialize, JsonSchema)]
86116
pub(crate) struct GetEntitiesForLiftingParams {
87117
/// Scope specifier: file glob ("src/auth/**"), hierarchy path, entity IDs, or "*"/"all".
88118
pub(crate) scope: String,
119+
/// Optional project root override for this call only.
120+
pub(crate) project_root: Option<String>,
89121
/// Batch index to retrieve (0-based). Omit or 0 for first batch.
90122
pub(crate) batch_index: Option<usize>,
91123
}
92124

93125
/// Parameters for the `submit_lift_results` tool.
94126
#[derive(Debug, Deserialize, JsonSchema)]
95127
pub(crate) struct SubmitLiftResultsParams {
128+
/// Optional project root override for this call only.
129+
pub(crate) project_root: Option<String>,
96130
/// JSON object mapping function names to feature arrays.
97131
/// Example: {"my_func": ["validate input", "return result"], "other": ["compute hash"]}
98132
pub(crate) features: String,
@@ -101,6 +135,8 @@ pub(crate) struct SubmitLiftResultsParams {
101135
/// Parameters for the `submit_hierarchy` tool.
102136
#[derive(Debug, Deserialize, JsonSchema)]
103137
pub(crate) struct SubmitHierarchyParams {
138+
/// Optional project root override for this call only.
139+
pub(crate) project_root: Option<String>,
104140
/// JSON object mapping file paths to 3-level hierarchy paths.
105141
/// Example: {"src/auth/login.rs": "Authentication/manage sessions/handle login"}
106142
pub(crate) assignments: String,
@@ -109,30 +145,51 @@ pub(crate) struct SubmitHierarchyParams {
109145
/// Parameters for the `get_files_for_synthesis` tool.
110146
#[derive(Debug, Deserialize, JsonSchema)]
111147
pub(crate) struct GetFilesForSynthesisParams {
148+
/// Optional project root override for this call only.
149+
pub(crate) project_root: Option<String>,
112150
/// Batch index to retrieve (0-based). Omit or 0 for first batch.
113151
pub(crate) batch_index: Option<usize>,
114152
}
115153

154+
/// Parameters for the `finalize_lifting` tool.
155+
#[derive(Debug, Default, Deserialize, JsonSchema)]
156+
pub(crate) struct FinalizeLiftingParams {
157+
/// Optional project root override for this call only.
158+
pub(crate) project_root: Option<String>,
159+
}
160+
161+
/// Parameters for the `get_routing_candidates` tool.
162+
#[derive(Debug, Default, Deserialize, JsonSchema)]
163+
pub(crate) struct GetRoutingCandidatesParams {
164+
/// Optional project root override for this call only.
165+
pub(crate) project_root: Option<String>,
166+
/// Batch index to retrieve (0-based). For large sets, returns paginated candidates.
167+
pub(crate) batch_index: Option<usize>,
168+
}
169+
116170
/// Parameters for the `submit_file_syntheses` tool.
117171
#[derive(Debug, Deserialize, JsonSchema)]
118172
pub(crate) struct SubmitFileSynthesesParams {
173+
/// Optional project root override for this call only.
174+
pub(crate) project_root: Option<String>,
119175
/// JSON object mapping file paths to comma-separated feature strings.
120176
/// Example: {"src/auth/login.rs": "handle user authentication, manage session tokens",
121177
/// "src/db/query.rs": "build SQL queries, execute database operations"}
122178
pub(crate) syntheses: String,
123179
}
124180

125-
/// Parameters for the `get_routing_candidates` tool.
126-
#[derive(Debug, Deserialize, JsonSchema)]
127-
pub(crate) struct GetRoutingCandidatesParams {
128-
/// Batch index to retrieve (0-based). For large sets, returns paginated candidates.
129-
#[serde(default)]
130-
pub(crate) batch_index: Option<usize>,
181+
/// Parameters for the `build_semantic_hierarchy` tool.
182+
#[derive(Debug, Default, Deserialize, JsonSchema)]
183+
pub(crate) struct BuildSemanticHierarchyParams {
184+
/// Optional project root override for this call only.
185+
pub(crate) project_root: Option<String>,
131186
}
132187

133188
/// Parameters for the `reconstruct_plan` tool.
134189
#[derive(Debug, Deserialize, JsonSchema)]
135190
pub(crate) struct ReconstructPlanParams {
191+
/// Optional project root override for this call only.
192+
pub(crate) project_root: Option<String>,
136193
/// Maximum number of entities per execution batch (default: 8).
137194
pub(crate) max_batch_size: Option<usize>,
138195
/// Include file-level Module entities in the schedule (default: false).
@@ -144,6 +201,8 @@ pub(crate) struct ReconstructPlanParams {
144201
pub(crate) struct ContextPackParams {
145202
/// The search query describing what context you need
146203
pub(crate) query: String,
204+
/// Optional project root override for this call only.
205+
pub(crate) project_root: Option<String>,
147206
/// Optional hierarchy scope to restrict search (e.g., 'Security/auth')
148207
pub(crate) scope: Option<String>,
149208
/// Target token budget for the packed context (default: 4000)
@@ -159,6 +218,8 @@ pub(crate) struct ContextPackParams {
159218
pub(crate) struct ImpactRadiusParams {
160219
/// The entity ID to compute impact from
161220
pub(crate) entity_id: String,
221+
/// Optional project root override for this call only.
222+
pub(crate) project_root: Option<String>,
162223
/// Traversal direction: 'upstream' (what depends on this), 'downstream' (what this depends on), or 'both'
163224
pub(crate) direction: Option<String>,
164225
/// Maximum traversal depth (default: 3). Use -1 for unlimited.
@@ -172,6 +233,8 @@ pub(crate) struct ImpactRadiusParams {
172233
/// Parameters for the `submit_routing_decisions` tool.
173234
#[derive(Debug, Deserialize, JsonSchema)]
174235
pub(crate) struct SubmitRoutingDecisionsParams {
236+
/// Optional project root override for this call only.
237+
pub(crate) project_root: Option<String>,
175238
/// JSON object mapping entity IDs to routing action.
176239
/// Value is a hierarchy path to route there, or "keep" to confirm current position.
177240
/// Example: {"src/auth.rs:validate_token": "Security/auth/validate", "src/db.rs:query": "keep"}
@@ -186,6 +249,8 @@ pub(crate) struct SubmitRoutingDecisionsParams {
186249
pub(crate) struct PlanChangeParams {
187250
/// The goal or intent of the change (e.g., "add rate limiting to API endpoints")
188251
pub(crate) goal: String,
252+
/// Optional project root override for this call only.
253+
pub(crate) project_root: Option<String>,
189254
/// Optional hierarchy scope to restrict search (e.g., 'Security/auth')
190255
pub(crate) scope: Option<String>,
191256
/// Maximum number of relevant entities to include (default: 15)
@@ -197,6 +262,8 @@ pub(crate) struct PlanChangeParams {
197262
pub(crate) struct FindPathsParams {
198263
/// Source entity ID
199264
pub(crate) source: String,
265+
/// Optional project root override for this call only.
266+
pub(crate) project_root: Option<String>,
200267
/// Target entity ID
201268
pub(crate) target: String,
202269
/// Maximum path length (default: 5). Use -1 for unlimited.
@@ -212,6 +279,8 @@ pub(crate) struct FindPathsParams {
212279
pub(crate) struct SliceBetweenParams {
213280
/// Entity IDs to connect (minimum 2)
214281
pub(crate) entity_ids: Vec<String>,
282+
/// Optional project root override for this call only.
283+
pub(crate) project_root: Option<String>,
215284
/// Maximum path length when searching for connections (default: 3)
216285
pub(crate) max_depth: Option<usize>,
217286
/// Include entity metadata (name, file, features) in output
@@ -223,6 +292,8 @@ pub(crate) struct SliceBetweenParams {
223292
pub(crate) struct AnalyzeHealthParams {
224293
/// Instability threshold above which entities are flagged as highly unstable (default: 0.7).
225294
pub(crate) instability_threshold: Option<f64>,
295+
/// Optional project root override for this call only.
296+
pub(crate) project_root: Option<String>,
226297
/// Minimum total degree for god object detection (default: 10).
227298
pub(crate) god_object_threshold: Option<usize>,
228299
/// Run Rabin-Karp token-based clone detection (reads source files from disk, slower). Default: false.
@@ -237,6 +308,8 @@ pub(crate) struct AnalyzeHealthParams {
237308
/// Parameters for the `semantic_snapshot` tool.
238309
#[derive(Debug, Deserialize, JsonSchema)]
239310
pub(crate) struct SemanticSnapshotParams {
311+
/// Optional project root override for this call only.
312+
pub(crate) project_root: Option<String>,
240313
/// Target token budget (default: 30000). Controls how much detail is included.
241314
pub(crate) token_budget: Option<usize>,
242315
/// Include dependency skeleton (default: true). Set to false to save tokens.
@@ -248,6 +321,8 @@ pub(crate) struct SemanticSnapshotParams {
248321
pub(crate) struct DetectCyclesParams {
249322
/// Maximum number of cycles to return (default: all). Use to limit output.
250323
pub(crate) max_cycles: Option<usize>,
324+
/// Optional project root override for this call only.
325+
pub(crate) project_root: Option<String>,
251326
/// Minimum cycle length to report (default: 2). Use 3+ to skip trivial 2-cycles.
252327
pub(crate) min_cycle_length: Option<usize>,
253328
/// Maximum cycle length to detect (default: 20, prevents exponential blowup)
@@ -273,6 +348,8 @@ pub(crate) struct DetectCyclesParams {
273348
/// Parameters for the `auto_lift` tool.
274349
#[derive(Deserialize, JsonSchema)]
275350
pub(crate) struct AutoLiftParams {
351+
/// Optional project root override for this call only.
352+
pub(crate) project_root: Option<String>,
276353
/// LLM provider: "anthropic", "openai", or any OpenAI-compatible endpoint.
277354
pub(crate) provider: String,
278355
/// API key for the provider. Use this OR api_key_env (not both). Prefer api_key_env to avoid exposing keys in tool call transcripts.

0 commit comments

Comments
 (0)