Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
707 changes: 31 additions & 676 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ zstd = "0.13.3"
ureq = "3"

# Embeddings
fastembed = "5.8.1"
fastembed = { version = "5.13.3", default-features = false, features = ["ort-load-dynamic", "hf-hub-native-tls"] }

# TOON format
toon-format = { version = "0.4.1", default-features = false }
Expand Down
59 changes: 31 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,21 +132,24 @@ Seven Rust crates, one MCP server binary, one CLI binary:
| `rpg-nav` | Search, fetch, explore, snapshot, TOON serialization |
| `rpg-lift` | Autonomous LLM lifting (Anthropic, OpenAI, OpenRouter, Gemini) |
| `rpg-cli` | CLI binary (`rpg-encoder`) |
| `rpg-mcp` | MCP server binary (`rpg-mcp-server`) with 27 tools |
| `rpg-mcp` | MCP server binary (`rpg-mcp-server`) with 28 tools |

---

## MCP Tools (27)
## MCP Tools (28)

Per-call `project_root` overrides target another repository without changing the active root. Semantic or embedding-backed queries may refresh the target root's `.rpg/embeddings.*` cache files to keep results up to date.

<details>
<summary><strong>Build & Maintain</strong> (4 tools)</summary>
<summary><strong>Build & Maintain</strong> (5 tools)</summary>

| Tool | Description |
|------|-------------|
| `set_project_root` | Switch the active repository root at runtime without restarting the MCP server |
| `build_rpg` | Index the codebase (run once, instant) |
| `update_rpg` | Incremental update from git changes |
| `reload_rpg` | Reload graph from disk after external changes |
| `rpg_info` | Graph statistics, hierarchy overview, per-area lifting coverage |
| `rpg_info` | Graph statistics, hierarchy overview, per-area lifting coverage. Supports optional per-call `project_root` override |

</details>

Expand All @@ -155,11 +158,11 @@ Seven Rust crates, one MCP server binary, one CLI binary:

| Tool | Description |
|------|-------------|
| `semantic_snapshot` | Whole-repo semantic understanding in one call (~25K tokens for 1000 entities) |
| `search_node` | Search entities by intent or keywords (hybrid embedding + lexical scoring) |
| `fetch_node` | Get entity metadata, source code, dependencies, and hierarchy context |
| `explore_rpg` | Traverse dependency graph (upstream, downstream, or both) |
| `context_pack` | Single-call search + fetch + explore with token budget |
| `semantic_snapshot` | Whole-repo semantic understanding in one call (~25K tokens for 1000 entities). Supports optional per-call `project_root` override |
| `search_node` | Search entities by intent or keywords (hybrid embedding + lexical scoring). Supports optional per-call `project_root` override |
| `fetch_node` | Get entity metadata, source code, dependencies, and hierarchy context. Supports optional per-call `project_root` override |
| `explore_rpg` | Traverse dependency graph (upstream, downstream, or both). Supports optional per-call `project_root` override |
| `context_pack` | Single-call search + fetch + explore with token budget. Supports optional per-call `project_root` override |

</details>

Expand All @@ -168,13 +171,13 @@ Seven Rust crates, one MCP server binary, one CLI binary:

| Tool | Description |
|------|-------------|
| `impact_radius` | BFS reachability analysis — "what depends on X?" |
| `plan_change` | Change planning — find relevant entities, modification order, blast radius |
| `find_paths` | K-shortest dependency paths between two entities |
| `slice_between` | Extract minimal connecting subgraph between entities |
| `analyze_health` | Code health: coupling, instability, god objects, clone detection |
| `detect_cycles` | Find circular dependencies and architectural cycles |
| `reconstruct_plan` | Dependency-safe reconstruction execution plan |
| `impact_radius` | BFS reachability analysis — "what depends on X?" Supports optional per-call `project_root` override |
| `plan_change` | Change planning — find relevant entities, modification order, blast radius. Supports optional per-call `project_root` override |
| `find_paths` | K-shortest dependency paths between two entities. Supports optional per-call `project_root` override |
| `slice_between` | Extract minimal connecting subgraph between entities. Supports optional per-call `project_root` override |
| `analyze_health` | Code health: coupling, instability, god objects, clone detection. Supports optional per-call `project_root` override |
| `detect_cycles` | Find circular dependencies and architectural cycles. Supports optional per-call `project_root` override |
| `reconstruct_plan` | Dependency-safe reconstruction execution plan. Supports optional per-call `project_root` override |

</details>

Expand All @@ -183,17 +186,17 @@ Seven Rust crates, one MCP server binary, one CLI binary:

| Tool | Description |
|------|-------------|
| `auto_lift` | One-call autonomous lifting via cheap LLM API (Haiku, GPT-4o-mini, OpenRouter, Gemini) |
| `lifting_status` | Dashboard — coverage, per-area progress, NEXT STEP |
| `get_entities_for_lifting` | Get entity source code for your agent to analyze |
| `submit_lift_results` | Submit the agent's semantic features back to the graph |
| `finalize_lifting` | Aggregate file-level features, rebuild hierarchy metadata |
| `get_files_for_synthesis` | Get file-level entity features for holistic synthesis |
| `submit_file_syntheses` | Submit holistic file-level summaries |
| `build_semantic_hierarchy` | Get domain discovery + hierarchy assignment prompts |
| `submit_hierarchy` | Apply hierarchy assignments to the graph |
| `get_routing_candidates` | Get entities needing semantic routing (drifted or newly lifted) |
| `submit_routing_decisions` | Submit routing decisions (hierarchy path or "keep") |
| `auto_lift` | One-call autonomous lifting via cheap LLM API (Haiku, GPT-4o-mini, OpenRouter, Gemini). Supports optional per-call `project_root` override |
| `lifting_status` | Dashboard — coverage, per-area progress, NEXT STEP. Supports optional per-call `project_root` override |
| `get_entities_for_lifting` | Get entity source code for your agent to analyze. Supports optional per-call `project_root` override |
| `submit_lift_results` | Submit the agent's semantic features back to the graph. Supports optional per-call `project_root` override |
| `finalize_lifting` | Aggregate file-level features, rebuild hierarchy metadata. Supports optional per-call `project_root` override |
| `get_files_for_synthesis` | Get file-level entity features for holistic synthesis. Supports optional per-call `project_root` override |
| `submit_file_syntheses` | Submit holistic file-level summaries. Supports optional per-call `project_root` override |
| `build_semantic_hierarchy` | Get domain discovery + hierarchy assignment prompts. Supports optional per-call `project_root` override |
| `submit_hierarchy` | Apply hierarchy assignments to the graph. Supports optional per-call `project_root` override |
| `get_routing_candidates` | Get entities needing semantic routing (drifted or newly lifted). Supports optional per-call `project_root` override |
| `submit_routing_decisions` | Submit routing decisions (hierarchy path or "keep"). Supports optional per-call `project_root` override |

</details>

Expand Down Expand Up @@ -241,7 +244,7 @@ claude mcp add rpg -- npx -y -p rpg-encoder rpg-mcp-server
}
```

The server auto-detects the project root from the current working directory no path argument needed.
The server auto-detects the project root from the startup directory by default. When no positional project path is provided, the binary falls back to its current working directory.

<details>
<summary><strong>CLI</strong></summary>
Expand Down
2 changes: 1 addition & 1 deletion crates/rpg-core/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ impl RPGraph {
}
}
let mut result: Vec<(String, Vec<String>)> = by_file.into_iter().collect();
result.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
result.sort_by_key(|entry| std::cmp::Reverse(entry.1.len()));
result
}

Expand Down
4 changes: 2 additions & 2 deletions crates/rpg-encoder/src/semantic_lifting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ pub fn apply_features(
pub fn aggregate_module_features(graph: &mut RPGraph) -> usize {
let module_data: Vec<(String, Vec<String>)> = graph
.file_index
.iter()
.filter_map(|(_, ids)| {
.values()
.filter_map(|ids| {
let module_id = ids.iter().find(|id| {
graph
.entities
Expand Down
4 changes: 2 additions & 2 deletions crates/rpg-lift/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ impl AnthropicProvider {
model: model.unwrap_or_else(|| Self::DEFAULT_MODEL.to_string()),
agent: ureq::Agent::new_with_config(
ureq::config::Config::builder()
.timeout_global(Some(std::time::Duration::from_secs(120)))
.timeout_global(Some(std::time::Duration::from_mins(2)))
.build(),
),
}
Expand Down Expand Up @@ -193,7 +193,7 @@ impl OpenAiProvider {
base_url: base_url.unwrap_or_else(|| Self::DEFAULT_BASE_URL.to_string()),
agent: ureq::Agent::new_with_config(
ureq::config::Config::builder()
.timeout_global(Some(std::time::Duration::from_secs(120)))
.timeout_global(Some(std::time::Duration::from_mins(2)))
.build(),
),
}
Expand Down
49 changes: 23 additions & 26 deletions crates/rpg-mcp/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,14 @@ pub(crate) const LARGE_SCOPE_BATCHES: usize = 10;
use anyhow::Result;
use rmcp::ServiceExt;
use rpg_core::storage;
use std::path::PathBuf;

use server::RpgServer;

#[tokio::main]
async fn main() -> Result<()> {
let project_root = std::env::args()
.nth(1)
.map(PathBuf::from)
.unwrap_or_else(|| std::env::current_dir().expect("failed to get current directory"));
let cli_args: Vec<String> = std::env::args().collect();
let cli_root = RpgServer::startup_project_root_arg(cli_args.iter().map(String::as_str));
let cwd = std::env::current_dir().expect("failed to get current directory");
let project_root = RpgServer::resolve_startup_project_root(cli_root.as_deref(), cwd);

eprintln!("RPG MCP server starting for: {}", project_root.display());

Expand All @@ -51,11 +49,13 @@ async fn main() -> Result<()> {

// Auto-update graph on startup if stale (structural-only, no LLM)
{
let mut lock = server.graph.write().await;
if let Some(ref mut graph) = *lock
let project_root = server.project_root().await;
let root_state = server.root_state(&project_root).await;
let mut root_state = root_state.write().await;
if let Some(ref mut graph) = root_state.graph
&& let (Some(base), Ok(head)) = (
&graph.base_commit.clone(),
rpg_encoder::evolution::get_head_sha(&server.project_root().await),
rpg_encoder::evolution::get_head_sha(&project_root),
)
{
if *base != head {
Expand All @@ -71,7 +71,7 @@ async fn main() -> Result<()> {
let qcache_result =
rpg_parser::paradigms::query_engine::QueryCache::compile_all(&paradigm_defs);
let active_defs = rpg_parser::paradigms::detect_paradigms_toml(
&server.project_root().await,
&project_root,
&detected_langs,
&paradigm_defs,
);
Expand All @@ -86,27 +86,21 @@ async fn main() -> Result<()> {
});
match rpg_encoder::evolution::run_update(
graph,
&server.project_root().await,
&project_root,
None,
pipeline.as_ref(),
) {
Ok(s) => {
graph.metadata.paradigms = paradigm_names;
let _ = storage::save(&server.project_root().await, graph);
// Persist stale entity IDs from the startup sync so
// lifting_status sees them on the first query. Every
// other path that produces a summary feeds
// `modified_entity_ids` into `stale_entity_ids`
// (`auto_sync_if_stale`, `update_rpg`). The startup
// path is the one exception — without this, modified
// entities from between the last lift and this startup
// are silently dropped across the session boundary.
let _ = storage::save(&project_root, graph);
let existing_ids: std::collections::HashSet<String> =
graph.entities.keys().cloned().collect();
{
let mut stale = server.stale_entity_ids.write().await;
let stale = &mut root_state.stale_entity_ids;
for id in &s.modified_entity_ids {
stale.insert(id.clone());
}
stale.retain(|id| graph.entities.contains_key(id));
stale.retain(|id| existing_ids.contains(id));
}
eprintln!(
" Auto-update complete: +{} -{} ~{}",
Expand All @@ -123,12 +117,15 @@ async fn main() -> Result<()> {
// changeset) match instead of redundantly re-running the
// workdir diff. Must use the real empty-workdir changeset
// hash (not an empty string) for the match to fire.
let project_root = server.project_root().await;
*server.last_auto_sync_head.write().await =
root_state.last_auto_sync_head =
rpg_encoder::evolution::get_head_sha(&project_root).ok();
*server.last_auto_sync_changeset.write().await =
root_state.last_auto_sync_changeset =
Some(RpgServer::compute_changeset_hash(&[], &project_root));
*server.last_auto_sync_workdir_paths.write().await = std::collections::HashSet::new();
root_state.last_auto_sync_workdir_paths = std::collections::HashSet::new();
drop(root_state);
server
.sync_default_root_compat_from_state(&project_root)
.await;
}
}

Expand Down
Loading
Loading