Skip to content

Commit 3d54419

Browse files
committed
refactor: rename MCP tools for clarity
- show_models → show_model (with action: desired/actual) - model_health + scrutinize → review_model (health as analysis variant) - refactor → refactor_model - scan extracted from refactor_model → scan_model 5 tools: show_model, review_model, set_model, refactor_model, scan_model
1 parent c7e5ab6 commit 3d54419

File tree

6 files changed

+177
-137
lines changed

6 files changed

+177
-137
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "dendrites"
3-
version = "0.1.3"
3+
version = "0.1.4"
44
edition = "2024"
55
description = "Domain Model Context Protocol Server - Architectural meta-layer for GitHub Copilot"
66
license = "MIT"

Formula/dendrites.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class Dendrites < Formula
44
license "MIT"
55
url "https://github.com/flavioaiello/dendrites/archive/refs/tags/v0.1.3.tar.gz"
66
sha256 "10a17122200edb97ed80242fd372d700cd6d32d9a587874d568d80dd4e6d0430"
7-
version "0.1.3"
7+
version "0.1.4"
88

99
head "https://github.com/flavioaiello/dendrites.git", branch: "main"
1010

src/mcp/tools.rs

Lines changed: 110 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -7,50 +7,47 @@ use crate::store::Store;
77
pub fn list_tools() -> Vec<ToolDefinition> {
88
vec![
99
ToolDefinition {
10-
name: "get_model".into(),
11-
description: "Returns both the desired and actual domain models, including bounded \
10+
name: "show_model".into(),
11+
description: "Returns the domain model for the specified state, including bounded \
1212
contexts, entities, services, events, rules, and conventions. \
13-
Shows pending changes status. \
14-
Use this before writing any new code to understand the system structure."
13+
Use 'desired' to see the target model, 'actual' to see what is \
14+
implemented. Shows pending changes status when viewing desired."
1515
.into(),
1616
input_schema: json!({
1717
"type": "object",
18-
"properties": {},
19-
"required": []
20-
}),
21-
},
22-
ToolDefinition {
23-
name: "model_health".into(),
24-
description: "Returns a structured health report for the domain model, computed \
25-
via Datalog inference from the CozoDB knowledge graph. Includes: \
26-
overall score (0-100), circular dependencies, layer violations, \
27-
missing invariants on aggregate roots, god contexts (>10 entities+services), \
28-
unsourced events, orphan contexts, and per-context complexity. \
29-
Use this to programmatically branch on model quality."
30-
.into(),
31-
input_schema: json!({
32-
"type": "object",
33-
"properties": {},
18+
"properties": {
19+
"action": {
20+
"type": "string",
21+
"enum": ["desired", "actual"],
22+
"description": "Which model state to show (default: desired)"
23+
}
24+
},
3425
"required": []
3526
}),
3627
},
3728
ToolDefinition {
38-
name: "scrutinize".into(),
29+
name: "review_model".into(),
3930
description: "Run Datalog-based analysis queries over the domain model knowledge graph. \
40-
Supports predefined analyses (transitive_deps, circular_deps, \
41-
layer_violations, impact_analysis, aggregate_quality, dependency_graph, \
42-
field_usage, method_search, shared_fields) \
43-
and arbitrary Datalog queries. All relations have a `state` column \
44-
('desired' | 'actual') for set-differencing. Relations: \
45-
context, context_dep, entity, service, service_dep, event, \
46-
value_object, repository, invariant, field, method, method_param, vo_rule."
31+
Supports predefined analyses: \
32+
'health' — structured health report (score 0-100, circular deps, \
33+
layer violations, missing invariants, god contexts, unsourced events, \
34+
orphan contexts, per-context complexity); \
35+
'transitive_deps', 'circular_deps', 'layer_violations', \
36+
'impact_analysis', 'aggregate_quality', 'dependency_graph', \
37+
'field_usage', 'method_search', 'shared_fields' — predefined graph queries; \
38+
'datalog' — arbitrary Datalog queries. \
39+
All relations have a `state` column ('desired' | 'actual') for \
40+
set-differencing. Relations: context, context_dep, entity, service, \
41+
service_dep, event, value_object, repository, invariant, field, method, \
42+
method_param, vo_rule."
4743
.into(),
4844
input_schema: json!({
4945
"type": "object",
5046
"properties": {
5147
"analysis": {
5248
"type": "string",
5349
"enum": [
50+
"health",
5451
"transitive_deps",
5552
"circular_deps",
5653
"layer_violations",
@@ -101,54 +98,79 @@ pub fn call_tool(
10198
args: &Value,
10299
) -> ToolCallResult {
103100
match name {
104-
"get_model" => {
101+
"show_model" => {
102+
let action = args.get("action")
103+
.and_then(|v| v.as_str())
104+
.unwrap_or("desired");
105105
let canonical = crate::store::cozo::canonicalize_path(workspace_path);
106106

107-
// Build overview from Datalog relations — no in-memory DomainRegistry
108-
let desired_overview = build_model_overview(store, &canonical, "desired");
109-
let actual_overview = build_model_overview(store, &canonical, "actual");
110-
111-
let has_actual = actual_overview.get("bounded_contexts")
112-
.and_then(|v| v.as_array())
113-
.is_some_and(|a| !a.is_empty());
114-
115-
// Use pure Datalog diff for sync check
116-
let (status, pending_count) = if has_actual {
117-
let changes = store.diff_graph(workspace_path).ok()
118-
.and_then(|v| v.get("pending_changes").cloned())
119-
.and_then(|v| v.as_array().cloned())
120-
.unwrap_or_default();
121-
if changes.is_empty() {
122-
("in_sync", 0)
123-
} else {
124-
("pending_changes", changes.len())
125-
}
126-
} else {
127-
("no_actual", 0)
128-
};
129-
130-
let overview = json!({
131-
"desired": desired_overview,
132-
"actual": if has_actual { actual_overview } else { json!(null) },
133-
"status": status,
134-
"pending_change_count": pending_count,
135-
});
136-
137-
text_result(serde_json::to_string(&overview).unwrap())
138-
}
107+
match action {
108+
"desired" => {
109+
let overview = build_model_overview(store, &canonical, "desired");
110+
111+
let actual_overview = build_model_overview(store, &canonical, "actual");
112+
let has_actual = actual_overview.get("bounded_contexts")
113+
.and_then(|v| v.as_array())
114+
.is_some_and(|a| !a.is_empty());
115+
116+
let (status, pending_count) = if has_actual {
117+
let changes = store.diff_graph(workspace_path).ok()
118+
.and_then(|v| v.get("pending_changes").cloned())
119+
.and_then(|v| v.as_array().cloned())
120+
.unwrap_or_default();
121+
if changes.is_empty() {
122+
("in_sync", 0)
123+
} else {
124+
("pending_changes", changes.len())
125+
}
126+
} else {
127+
("no_actual", 0)
128+
};
139129

140-
"model_health" => {
141-
match store.model_health(workspace_path) {
142-
Ok(health) => text_result(serde_json::to_string(&health).unwrap()),
143-
Err(e) => error_result(format!("model_health query failed: {e}")),
130+
let result = json!({
131+
"state": "desired",
132+
"model": overview,
133+
"status": status,
134+
"pending_change_count": pending_count,
135+
});
136+
text_result(serde_json::to_string(&result).unwrap())
137+
}
138+
"actual" => {
139+
let overview = build_model_overview(store, &canonical, "actual");
140+
let has_actual = overview.get("bounded_contexts")
141+
.and_then(|v| v.as_array())
142+
.is_some_and(|a| !a.is_empty());
143+
144+
if !has_actual {
145+
let result = json!({
146+
"state": "actual",
147+
"model": null,
148+
"message": "No actual model exists. Run scan_model to extract it from source code."
149+
});
150+
text_result(serde_json::to_string(&result).unwrap())
151+
} else {
152+
let result = json!({
153+
"state": "actual",
154+
"model": overview,
155+
});
156+
text_result(serde_json::to_string(&result).unwrap())
157+
}
158+
}
159+
_ => error_result(format!("Unknown action '{action}'. Use 'desired' or 'actual'.")),
144160
}
145161
}
146162

147-
"scrutinize" => {
163+
"review_model" => {
148164
let analysis = args["analysis"].as_str().unwrap_or("");
149165
let canonical = crate::store::cozo::canonicalize_path(workspace_path);
150166

151167
match analysis {
168+
"health" => {
169+
match store.model_health(workspace_path) {
170+
Ok(health) => text_result(serde_json::to_string(&health).unwrap()),
171+
Err(e) => error_result(format!("health analysis failed: {e}")),
172+
}
173+
}
152174
"transitive_deps" => {
153175
let context = match args["context"].as_str() {
154176
Some(c) => c,
@@ -351,7 +373,7 @@ pub fn call_tool(
351373
Err(e) => error_result(format!("Shared fields query failed: {e}")),
352374
}
353375
}
354-
_ => error_result(format!("Unknown analysis type: '{}'. Valid types: transitive_deps, circular_deps, layer_violations, impact_analysis, aggregate_quality, dependency_graph, field_usage, method_search, shared_fields, datalog", analysis)),
376+
_ => error_result(format!("Unknown analysis type: '{}'. Valid types: health, transitive_deps, circular_deps, layer_violations, impact_analysis, aggregate_quality, dependency_graph, field_usage, method_search, shared_fields, datalog", analysis)),
355377
}
356378
}
357379

@@ -683,7 +705,7 @@ mod tests {
683705
#[test]
684706
fn test_list_tools_count() {
685707
let tools = list_tools();
686-
assert_eq!(tools.len(), 3);
708+
assert_eq!(tools.len(), 2);
687709
}
688710

689711
#[test]
@@ -694,29 +716,38 @@ mod tests {
694716
// Save desired + accept to create actual
695717
store.save_desired(ws, &model).unwrap();
696718
store.accept(ws).unwrap();
697-
let result = call_tool(&store, ws, "get_model", &json!({}));
719+
let result = call_tool(&store, ws, "show_model", &json!({}));
698720
let text = match &result.content[0] {
699721
ContentBlock::Text { text } => text,
700722
};
701723
let parsed: Value = serde_json::from_str(text).unwrap();
702-
assert!(parsed.get("desired").is_some());
703-
assert!(parsed.get("actual").is_some());
724+
assert_eq!(parsed["state"], "desired");
725+
assert!(parsed.get("model").is_some());
704726
assert_eq!(parsed["status"], "in_sync");
705727
assert_eq!(parsed["pending_change_count"], 0);
728+
729+
// Also verify actual action
730+
let result = call_tool(&store, ws, "show_model", &json!({"action": "actual"}));
731+
let text = match &result.content[0] {
732+
ContentBlock::Text { text } => text,
733+
};
734+
let parsed: Value = serde_json::from_str(text).unwrap();
735+
assert_eq!(parsed["state"], "actual");
736+
assert!(parsed.get("model").is_some());
706737
}
707738

708739
#[test]
709740
fn test_overview_no_actual_shows_status() {
710741
let store = test_store();
711742
let ws = "/tmp/test-no-actual";
712743
store.save_desired(ws, &test_model()).unwrap();
713-
let result = call_tool(&store, ws, "get_model", &json!({}));
744+
let result = call_tool(&store, ws, "show_model", &json!({"action": "actual"}));
714745
let text = match &result.content[0] {
715746
ContentBlock::Text { text } => text,
716747
};
717748
let parsed: Value = serde_json::from_str(text).unwrap();
718-
assert_eq!(parsed["status"], "no_actual");
719-
assert_eq!(parsed["actual"], json!(null));
749+
assert_eq!(parsed["state"], "actual");
750+
assert_eq!(parsed["model"], json!(null));
720751
}
721752

722753
#[test]
@@ -731,7 +762,7 @@ mod tests {
731762
}
732763
store.save_desired(ws, &model).unwrap();
733764

734-
let result = call_tool(&store, ws, "scrutinize", &json!({
765+
let result = call_tool(&store, ws, "review_model", &json!({
735766
"analysis": "circular_deps"
736767
}));
737768
let text = match &result.content[0] {
@@ -767,7 +798,7 @@ mod tests {
767798
});
768799
store.save_desired(ws, &model).unwrap();
769800

770-
let result = call_tool(&store, ws, "scrutinize", &json!({
801+
let result = call_tool(&store, ws, "review_model", &json!({
771802
"analysis": "transitive_deps",
772803
"context": "Notifications"
773804
}));
@@ -789,7 +820,7 @@ mod tests {
789820
let model = test_model();
790821
store.save_desired(ws, &model).unwrap();
791822

792-
let result = call_tool(&store, ws, "scrutinize", &json!({
823+
let result = call_tool(&store, ws, "review_model", &json!({
793824
"analysis": "datalog",
794825
"query": "?[name] := *entity{workspace: $ws, name}"
795826
}));
@@ -808,7 +839,7 @@ mod tests {
808839
let model = test_model();
809840
store.save_desired(ws, &model).unwrap();
810841

811-
let result = call_tool(&store, ws, "scrutinize", &json!({
842+
let result = call_tool(&store, ws, "review_model", &json!({
812843
"analysis": "dependency_graph"
813844
}));
814845
let text = match &result.content[0] {
@@ -823,7 +854,7 @@ mod tests {
823854
#[test]
824855
fn test_query_model_missing_param() {
825856
let store = test_store();
826-
let result = call_tool(&store, "/tmp/x", "scrutinize", &json!({
857+
let result = call_tool(&store, "/tmp/x", "review_model", &json!({
827858
"analysis": "transitive_deps"
828859
}));
829860
assert_eq!(result.is_error, Some(true));

0 commit comments

Comments
 (0)