From b99c03aa5d9ab4ef5b732a1b9bbee25a4021d9e7 Mon Sep 17 00:00:00 2001 From: Ville Laitila Date: Sat, 14 Feb 2026 01:43:03 +0200 Subject: [PATCH] Claude Code profile: plain text TOON output, include_descendants, auto-load - All 4 data tools now return plain text using TOON line notation instead of JSON-wrapped responses. Removes redundant fields (format, count, direction). - Add include_descendants parameter to get_element_dependencies: recursively collects children's dependencies using relative paths (no leading /) to identify which descendant owns each dependency. - Add --auto-load and --default-scope CLI args for startup model loading. - Add default_model_id and load_model_sync to ModelManager. - Update all documentation to reflect new output format. --- CLAUDE.md | 2 +- README.md | 99 ++--- SGRAPH_FOR_CLAUDE_CODE.md | 328 ++++++++-------- docs/GLOBAL_CLAUDE_MD_SNIPPET.md | 2 +- docs/SGRAPH_MCP_INTEGRATION_ANALYSIS.md | 298 +++++++++++++++ docs/SGRAPH_MCP_TROUBLESHOOTING.md | 170 +++++++++ src/core/model_manager.py | 41 +- src/profiles/base.py | 14 +- src/profiles/claude_code.py | 477 ++++++++++++++---------- src/server.py | 30 ++ src/services/search_service.py | 18 +- 11 files changed, 1035 insertions(+), 444 deletions(-) create mode 100644 docs/SGRAPH_MCP_INTEGRATION_ANALYSIS.md create mode 100644 docs/SGRAPH_MCP_TROUBLESHOOTING.md diff --git a/CLAUDE.md b/CLAUDE.md index 214d162..30f5508 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,7 +92,7 @@ The server supports multiple profiles in `src/profiles/`: | Profile | Tools | Description | |---------|-------|-------------| | `legacy` | 14 | Full original tool set (default, backwards compatible) | -| `claude-code` | 5 | Optimized for Claude Code - consolidated tools, TOON output format | +| `claude-code` | 5 | Optimized for Claude Code - plain text TOON output, `include_descendants` support | **Claude Code profile tools** (see `SGRAPH_FOR_CLAUDE_CODE.md`): - `sgraph_search_elements` - Find symbols by pattern diff --git a/README.md b/README.md index 0227960..6e0778a 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,28 @@ The sgraph-mcp-server uses a **modular architecture** designed for maintainabili See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed design documentation. -### Current MCP Tools +### MCP Tools by Profile + +#### Claude Code Profile (5 tools, plain text output) + +All Claude Code tools return **plain text using TOON line notation** instead of JSON, minimizing token usage. + +| Tool | Purpose | Output Format | +|------|---------|---------------| +| `sgraph_load_model` | Load and cache a model | JSON (metadata only) | +| `sgraph_search_elements` | Find elements by name pattern | `N/M matches` + `/path [type] name` lines | +| `sgraph_get_element_dependencies` | Query incoming/outgoing deps | `-> /target (type)` or `/source (type) ->` | +| `sgraph_get_element_structure` | Explore hierarchy without reading source | Indented `/path [type] name` tree | +| `sgraph_analyze_change_impact` | Multi-level impact analysis | Summary + paths grouped by aggregation level | + +Key features: +- **`include_descendants`** on dependencies: shows which child element owns each dependency using relative paths (no leading `/`) +- **`result_level`** aggregation: same data at function/file/directory/repository granularity +- **`scope_path`** filtering: limit searches to subtrees, with configurable default scope + +See [SGRAPH_FOR_CLAUDE_CODE.md](SGRAPH_FOR_CLAUDE_CODE.md) for full tool reference, output examples, and workflows. + +#### Legacy Profile (14 tools, JSON output) **Basic Operations:** - `sgraph_load_model` - Load and cache an sgraph model from file @@ -64,69 +85,6 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed design documentation. - `sgraph_get_model_overview` - Get hierarchical overview of model structure with configurable depth - `sgraph_get_high_level_dependencies` - Get module-level dependencies aggregated at directory level with metrics -#### Search Examples - -```python -# Find all functions containing "test" in their name -sgraph_search_elements_by_name(model_id="abc123", pattern=".*test.*", element_type="function") - -# Get all classes in a specific directory -sgraph_get_elements_by_type(model_id="abc123", element_type="class", scope_path="/project/src/models") - -# Find elements with specific attributes -sgraph_search_elements_by_attributes( - model_id="abc123", - attribute_filters={"visibility": "public", "complexity": "high"} -) -``` - -#### Bulk Analysis Examples - -```python -# Analyze dependencies within a module subtree -sgraph_get_subtree_dependencies( - model_id="abc123", - root_path="/project/src/auth", - include_external=True, - max_depth=3 -) - -# Get transitive dependency chain from an element -sgraph_get_dependency_chain( - model_id="abc123", - element_path="/project/src/auth/login.py/LoginHandler", - direction="outgoing", - max_depth=2 -) - -# Efficiently retrieve multiple elements -sgraph_get_multiple_elements( - model_id="abc123", - element_paths=[ - "/project/src/auth/login.py/LoginHandler", - "/project/src/auth/session.py/SessionManager", - "/project/src/database/user.py/User" - ] -) - -# Get hierarchical overview of the model structure -sgraph_get_model_overview( - model_id="abc123", - max_depth=3, - include_counts=True -) - -# Get high-level module dependencies with metrics -sgraph_get_high_level_dependencies( - model_id="abc123", - scope_path="/project/src", # Optional: limit to src directory - aggregation_level=2, # Aggregate at /project/module level - min_dependency_count=3, # Only show dependencies with 3+ connections - include_external=True, # Include external dependencies - include_metrics=True # Calculate coupling metrics and hotspots -) -``` - ## SGraph Data Structure SGraph models represent software structures as hierarchical graphs where: @@ -206,10 +164,10 @@ Performance tests use real models to verify operations complete within acceptabl The server supports multiple profiles optimized for different use cases: -| Profile | Tools | Use Case | -|---------|-------|----------| -| `legacy` | 14 | Full tool set (backwards compatible, default) | -| `claude-code` | 5 | AI-assisted software development (optimized for Claude Code) | +| Profile | Tools | Output | Use Case | +|---------|-------|--------|----------| +| `legacy` | 14 | JSON | Full tool set (backwards compatible, default) | +| `claude-code` | 5 | Plain text (TOON) | AI-assisted development (token-optimized) | ### Run the server @@ -217,7 +175,7 @@ The server supports multiple profiles optimized for different use cases: # Default (legacy profile with all 14 tools) uv run python -m src.server -# Claude Code optimized (5 consolidated tools, 60% fewer tokens) +# Claude Code optimized (5 tools, plain text TOON output) uv run python -m src.server --profile claude-code # Explicit legacy @@ -252,8 +210,7 @@ For Claude Code or Cursor, you can also use a `.mcp.json` file in your project r ### Profile Documentation -- **Claude Code**: See [SGRAPH_FOR_CLAUDE_CODE.md](SGRAPH_FOR_CLAUDE_CODE.md) for tool reference and workflows -- **Genealogy**: See [SGRAPH_FOR_GENEALOGY.md](SGRAPH_FOR_GENEALOGY.md) for family tree navigation guide +- **Claude Code**: See [SGRAPH_FOR_CLAUDE_CODE.md](SGRAPH_FOR_CLAUDE_CODE.md) for tool reference, output examples, and workflows ## About SGraph diff --git a/SGRAPH_FOR_CLAUDE_CODE.md b/SGRAPH_FOR_CLAUDE_CODE.md index 4f767a8..ef97066 100644 --- a/SGRAPH_FOR_CLAUDE_CODE.md +++ b/SGRAPH_FOR_CLAUDE_CODE.md @@ -16,9 +16,14 @@ SGraph provides **pre-computed dependency graphs** that answer architectural que ```bash # Start the server with Claude Code profile (5 optimized tools) uv run python -m src.server --profile claude-code + +# With auto-loaded model and default scope +uv run python -m src.server --profile claude-code \ + --auto-load /path/to/model.xml.zip \ + --default-scope /Project/repo ``` -Add to Claude Code's MCP config: +Add to Claude Code's MCP config (`.mcp.json` in project root): ```json { "mcpServers": { @@ -40,20 +45,30 @@ Add to Claude Code's MCP config: | `sgraph_get_element_structure` | Explore hierarchy | Instead of Read to see contents | | `sgraph_analyze_change_impact` | Full impact analysis | Before any public interface change | +## Output Format + +All tools return **plain text using TOON (Token-Optimized Object Notation) line notation** - not JSON. This minimizes token usage and is easier for LLMs to parse. + +Each tool has its own TOON line format: +- **search_elements**: `/path [type] name` +- **get_element_dependencies**: `-> /target (type)` or `/source (type) ->` +- **get_element_structure**: indented `/path [type] name` +- **analyze_change_impact**: sections with paths grouped by aggregation level + --- ## Tool Reference ### sgraph_load_model -Load a graph model file. Required before using other tools. +Load a graph model file. Required before using other tools (unless `--auto-load` is configured). ```python sgraph_load_model(path="/path/to/model.xml.zip") -# Returns: {"model_id": "abc123..."} +# Returns: {"model_id": "abc123...", "cached": true, "default_scope": "..."} ``` -**Note**: Save the `model_id` - you'll need it for all subsequent calls. +If `--auto-load` is configured, the model loads at startup and `model_id` can be omitted from all other calls. --- @@ -63,31 +78,31 @@ Find code elements by name pattern. **Use instead of grep for symbol lookup.** ```python # Find all Manager classes -sgraph_search_elements( - model_id="...", - query=".*Manager", - element_types=["class"] -) +sgraph_search_elements(query="*Manager*", element_types=["class"]) -# Find functions starting with "validate" in auth module +# Find files matching a pattern in a specific subtree sgraph_search_elements( - model_id="...", - query="^validate.*", - scope_path="/project/src/auth", - element_types=["function"] + query="*Service*", + scope_path="/Project/src/auth", + element_types=["file"] ) ``` -**Output (TOON format)**: +**Output:** ``` +3/12 matches /project/src/core/model_manager.py/ModelManager [class] ModelManager /project/src/auth/session_manager.py/SessionManager [class] SessionManager +/project/src/cache/cache_manager.py/CacheManager [class] CacheManager ``` -**When to use**: -- You know a class/function name but not its location -- Finding all implementations matching a pattern -- Locating a symbol before querying its dependencies +First line: `shown/total matches`. Each subsequent line: `/path [type] name`. + +**Parameters:** +- `query`: Wildcards (`*Service*`), regex (`.*Service.*`), or substring (`Service`) +- `scope_path`: Limit search to subtree (uses server default scope if not set) +- `element_types`: Filter by `["class", "function", "method", "file", "directory"]` +- `max_results`: Limit results (default 50) --- @@ -96,51 +111,59 @@ sgraph_search_elements( **THE KEY TOOL** - Query dependencies with abstraction level control. ```python -# What functions call this function? (before changing signature) +# What calls this function? (raw detail) sgraph_get_element_dependencies( - model_id="...", element_path="/project/src/auth/manager.py/AuthManager/validate", - direction="incoming", - result_level=None # Raw function-level detail + direction="incoming" ) -# What FILES depend on this file? (planning file move) +# What repos does this repo depend on? sgraph_get_element_dependencies( - model_id="...", - element_path="/project/src/auth/manager.py", - direction="incoming", - result_level=4 # Aggregated to file level + element_path="/project/src/myrepo", + direction="outgoing", + result_level=2, + include_descendants=True ) +``` -# What does this class use? (understanding implementation) -sgraph_get_element_dependencies( - model_id="...", - element_path="/project/src/api/UserService", - direction="outgoing" -) +**Output (element's own dependencies):** ``` +outgoing (2): +-> /project/src/db/user_repo.py/UserRepo (call) +-> /project/src/crypto/service.py/hash (call) +incoming (3): +/project/src/api/endpoints.py/get_user (call) -> +/project/src/api/endpoints.py/delete_user (call) -> +/project/src/middleware/auth.py/check (call) -> +``` + +**Output (with `include_descendants=True`):** +``` +outgoing (4): +-> /external/requests (import) +AuthManager/validate -> /project/src/db/user_repo.py/UserRepo (call) +AuthManager/validate -> /project/src/crypto/service.py/hash (call) +AuthManager/refresh -> /project/src/cache/store.py/TokenStore (call) +``` + +Relative paths (no leading `/`) identify which descendant owns the dependency. Absolute paths (leading `/`) are the targets. -**Direction explained**: -- `"incoming"`: What **uses** this element (callers, importers) → impact analysis -- `"outgoing"`: What this element **uses** (callees, imports) → understanding context -- `"both"`: Both in one call +**Parameters:** +| Parameter | Description | +|-----------|-------------| +| `direction` | `"incoming"`, `"outgoing"`, or `"both"` | +| `result_level` | Aggregate: `None`=raw, `4`=file, `3`=directory, `2`=repository | +| `include_descendants` | `false` (default): only this element. `true`: include children recursively | +| `include_external` | Include third-party dependencies (default `true`) | -**result_level explained** (THE KEY FEATURE): -| Level | Meaning | Example | -|-------|---------|---------| +**result_level** controls abstraction - same data, different granularity: +| Level | Meaning | Example: 41 raw deps becomes... | +|-------|---------|--------------------------------| | `None` | Raw (as captured) | 41 individual function calls | | `4` | File level | 5 unique files | | `3` | Directory level | 2 unique directories | | `2` | Repository level | 2 unique repos | -Same underlying data, different abstraction. One parameter changes the view. - -**Output (TOON format)**: -``` -/project/src/api/endpoints.py/get_user -> /project/src/auth/manager.py/validate -/project/src/middleware/auth.py/check -> /project/src/auth/manager.py/validate -``` - --- ### sgraph_get_element_structure @@ -148,44 +171,31 @@ Same underlying data, different abstraction. One parameter changes the view. Explore what's inside a file, class, or directory **without reading source code**. ```python -# What classes/functions are in this file? -sgraph_get_element_structure( - model_id="...", - element_path="/project/src/core/model_manager.py", - max_depth=2 -) - # What's in the services directory? sgraph_get_element_structure( - model_id="...", element_path="/project/src/services", max_depth=2 ) ``` -**Output**: -```json -{ - "path": "/project/src/core/model_manager.py", - "type": "file", - "name": "model_manager.py", - "children": [ - { - "path": ".../ModelManager", - "type": "class", - "name": "ModelManager", - "children": [ - {"path": ".../load_model", "type": "method", "name": "load_model"}, - {"path": ".../get_model", "type": "method", "name": "get_model"} - ] - } - ] -} +**Output:** ``` +/project/src/services [dir] services + /project/src/services/auth_service.py [file] auth_service.py + /project/src/services/auth_service.py/AuthService [class] AuthService + /project/src/services/auth_service.py/validate_token [function] validate_token + /project/src/services/user_service.py [file] user_service.py + /project/src/services/user_service.py/UserService [class] UserService +``` + +Indentation shows hierarchy. Each line: `/path [type] name`. + +**Parameters:** +- `max_depth`: `1` = direct children only, `2` = two levels (usually sufficient), `3+` = deeper -**When to use**: +**When to use:** - Before deciding which file to Read (cheaper exploration) -- Understanding class structure without source +- Understanding class structure without reading source - Directory exploration without ls/find --- @@ -196,38 +206,34 @@ sgraph_get_element_structure( ```python sgraph_analyze_change_impact( - model_id="...", element_path="/project/src/auth/manager.py/AuthManager/validate" ) ``` -**Output**: -```json -{ - "element": "/project/src/auth/manager.py/AuthManager/validate", - "element_type": "method", - "incoming_by_level": { - "detailed": [ - "/project/src/api/endpoints.py/UserEndpoint/get_profile", - "/project/src/api/endpoints.py/AdminEndpoint/delete_user", - "/project/src/middleware/auth.py/require_auth" - ], - "file": ["/project/src/api", "/project/src/middleware"], - "module": ["/project/src"] - }, - "summary": { - "incoming_count": 3, - "files_affected": 2, - "modules_affected": 1 - } -} +**Output:** +``` +impact: 3 callers, 2 files, 1 modules + +detailed (3): +/project/src/api/endpoints.py/UserEndpoint/get_profile +/project/src/api/endpoints.py/AdminEndpoint/delete_user +/project/src/middleware/auth.py/require_auth + +by_file (2): +/project/src/api +/project/src/middleware + +by_module (1): +/project/src ``` -**When to use**: -- Before changing function signature → see all call sites -- Before renaming class → see all importers -- Before deleting code → verify nothing depends on it -- Planning refactoring → understand blast radius +First line is a summary. Then three sections show the same callers aggregated at different levels. + +**When to use:** +- Before changing function signature -> see all call sites +- Before renaming class -> see all importers +- Before deleting code -> verify nothing depends on it +- Planning refactoring -> understand blast radius --- @@ -240,11 +246,11 @@ sgraph_analyze_change_impact( ``` 1. LOCATE the function: sgraph_search_elements(query="validate", scope_path="/project/src/auth") - → /project/src/auth/manager.py/AuthManager/validate + -> /project/src/auth/manager.py/AuthManager/validate [method] validate 2. CHECK IMPACT before changing: sgraph_analyze_change_impact(element_path="...validate") - → 3 callers in 2 files + -> impact: 3 callers, 2 files, 1 modules 3. PLAN changes: - Modify validate() signature @@ -256,24 +262,30 @@ sgraph_analyze_change_impact( 5. VERIFY: Run tests ``` -### Example 2: Understanding a New Codebase +### Example 2: Understanding Cross-Repo Dependencies -**Task**: Understand how authentication works +**Task**: What external packages does this repo depend on? ``` -1. FIND auth-related classes: - sgraph_search_elements(query=".*[Aa]uth.*", element_types=["class"]) - → AuthManager, AuthMiddleware, AuthConfig - -2. EXPLORE structure of main class: - sgraph_get_element_structure(element_path=".../AuthManager", max_depth=2) - → Shows: validate(), refresh_token(), revoke() methods - -3. CHECK what AuthManager depends on: - sgraph_get_element_dependencies(element_path=".../AuthManager", direction="outgoing") - → Uses: TokenStore, UserRepository, CryptoService - -4. NOW read specific files with full context understanding +1. QUERY at repository level with descendants: + sgraph_get_element_dependencies( + element_path="/Project/repo", + direction="outgoing", + result_level=2, + include_descendants=True + ) + -> outgoing (6): + -> /Project/External + -> /Project/OtherRepo + Repo/src/api.csproj -> /Project/Utilities + Repo/src/service.csproj -> /Project/SharedLib + ... + +2. DRILL DOWN into specific dependency: + sgraph_get_element_dependencies( + element_path="/Project/repo/src/service.csproj", + direction="outgoing" + ) ``` ### Example 3: Safe Refactoring @@ -283,11 +295,14 @@ sgraph_analyze_change_impact( ``` 1. ANALYZE full impact: sgraph_analyze_change_impact(element_path=".../UserService") - → incoming_count: 15, files_affected: 8, modules_affected: 3 + -> impact: 15 callers, 8 files, 3 modules 2. GET detailed callers: sgraph_get_element_dependencies(element_path=".../UserService", direction="incoming") - → List of all 15 import sites + -> incoming (15): + /project/src/api/user_endpoint.py/get_user (call) -> + /project/src/api/admin.py/list_users (call) -> + ... 3. PLAN: Update all 15 import statements after move @@ -307,44 +322,15 @@ sgraph_analyze_change_impact( 3. **Check impact before changes** - avoid "fix one, break three" cycles 4. **Use structure instead of Read** for exploration - much cheaper 5. **Start with high result_level**, drill down if needed +6. **Use `include_descendants=True`** to see which child element causes each dependency +7. **Widen `scope_path`** when searching for symbols in other repos (e.g., NuGet sources) ### Don't Do This -1. ❌ Use grep for finding symbols (lossy, matches comments/strings) -2. ❌ Read files to understand structure (token expensive) -3. ❌ Modify code without checking incoming dependencies -4. ❌ Make multiple calls when one tool returns all levels - ---- - -## Comparison: Without vs With SGraph - -### Without SGraph (Native Claude Code) - -``` -Claude: *wants to modify validate() signature* -Claude: grep -r "validate" src/ -→ 247 matches (comments, strings, unrelated functions) -Claude: *reads 10 files trying to find actual callers* -Claude: *misses one caller in obscure file* -Claude: *makes change* -Tests: FAIL - missed caller breaks -Claude: *reads error, finds missed file, fixes* -Tests: FAIL - another missed caller -... (repeat 5×, context saturated) -``` - -### With SGraph - -``` -Claude: *wants to modify validate() signature* -Claude: sgraph_analyze_change_impact(element_path=".../validate") -→ Exactly 3 callers in 2 files, with full paths -Claude: *modifies validate() and all 3 call sites* -Tests: PASS -``` - -**Result**: Proactive impact analysis replaces reactive error fixing. +1. Don't use grep for finding symbols (lossy, matches comments/strings) +2. Don't Read files to understand structure (token expensive) +3. Don't modify code without checking incoming dependencies +4. Don't make multiple calls when one tool returns all levels --- @@ -353,29 +339,27 @@ Tests: PASS All paths in SGraph are hierarchical: ``` -/project-name/repository/src/directory/file.py/ClassName/method_name +/Organization/Category/repository/src/directory/file.py/ClassName/method_name ``` Examples: -- File: `/myproject/backend/src/auth/manager.py` -- Class: `/myproject/backend/src/auth/manager.py/AuthManager` -- Method: `/myproject/backend/src/auth/manager.py/AuthManager/validate` -- External: `/myproject/External/Python/requests` +- Repository: `/TalenomSoftware/Online/talenom.online.invoicepayment5.api` +- File: `/TalenomSoftware/Online/repo/src/auth/manager.py` +- Class: `/TalenomSoftware/Online/repo/src/auth/manager.py/AuthManager` +- External: `/TalenomSoftware/External/Python/requests` Paths are **unambiguous** - no confusion about which `validate` you mean. ---- - -## TOON Output Format +## Scope and Default Scope -Tools return **TOON (Token-Optimized Object Notation)** - 50-60% fewer tokens than JSON. +The `--default-scope` CLI parameter limits searches to a subtree by default: +```bash +uv run python -m src.server --profile claude-code \ + --default-scope /TalenomSoftware/Online/my-repo ``` -# JSON (~45 tokens) -{"source":"src/api/endpoints.py/get_user","target":"src/auth/manager.py/validate","type":"call"} -# TOON (~15 tokens) -src/api/endpoints.py/get_user -> src/auth/manager.py/validate (call) +To search outside the default scope, pass `scope_path` explicitly: +```python +sgraph_search_elements(query="*CompanyIdentity*", scope_path="/TalenomSoftware") ``` - -Line-oriented format is easier to scan and cheaper to process. diff --git a/docs/GLOBAL_CLAUDE_MD_SNIPPET.md b/docs/GLOBAL_CLAUDE_MD_SNIPPET.md index d9ee273..07b508a 100644 --- a/docs/GLOBAL_CLAUDE_MD_SNIPPET.md +++ b/docs/GLOBAL_CLAUDE_MD_SNIPPET.md @@ -107,7 +107,7 @@ sgraph-mcp # Starts with claude-code profile (default) |------|---------| | `sgraph_load_model` | Load model file, get model_id | | `sgraph_search_elements` | Find symbols by pattern (replaces grep for code search) | -| `sgraph_get_element_dependencies` | **KEY TOOL** - query dependencies with `result_level` abstraction | +| `sgraph_get_element_dependencies` | **KEY TOOL** - query dependencies with `result_level` abstraction and `include_descendants` | | `sgraph_get_element_structure` | Explore hierarchy without reading source | | `sgraph_analyze_change_impact` | Check what breaks before modifying code | diff --git a/docs/SGRAPH_MCP_INTEGRATION_ANALYSIS.md b/docs/SGRAPH_MCP_INTEGRATION_ANALYSIS.md new file mode 100644 index 0000000..5b53ae8 --- /dev/null +++ b/docs/SGRAPH_MCP_INTEGRATION_ANALYSIS.md @@ -0,0 +1,298 @@ +# Sgraph MCP Integration: Syvällinen analyysi + +Dokumentti kuvaa kaikki vaikeudet ja opitut asiat sgraph-mcp-serverin integroinnissa Claude Codeen. + +## Yhteenveto + +Integraatio onnistui lopulta, mutta prosessissa kohdattiin useita abstraktiotasojen ongelmia: + +1. **MCP-protokollan parametrirakenne** - suurin ongelma +2. **Sgraph-kirjaston API-erot** - dokumentaatio vs. todellisuus +3. **Async/await -kontekstit** - Python asyncio -haasteet +4. **Mallin rakenne** - hierarkian ymmärtäminen + +--- + +## 1. MCP-protokollan parametrirakenne (KRIITTINEN) + +### Ongelma + +FastMCP käyttää Pydantic-malleja parametrien validointiin. Kun työkalu määritellään näin: + +```python +class LoadModelInput(BaseModel): + path: str + +@mcp.tool() +async def sgraph_load_model(input: LoadModelInput): + ... +``` + +MCP-protokolla odottaa parametrit **`input`-avaimen alle**: + +```json +{"input": {"path": "/path/to/model.xml"}} +``` + +Mutta intuitiivinen oletus on lähettää: + +```json +{"path": "/path/to/model.xml"} +``` + +### Miksi tämä on ongelma + +1. **Dokumentaatio ei kerro tästä selvästi** - FastMCP:n README ei korosta tätä +2. **Virheilmoitus on epäselvä** - "Field required: input" ei kerro mitä tehdä +3. **Claude Code saattaa tehdä tämän automaattisesti** - mutta ei aina + +### Ratkaisu + +Joko: +- Lähetä parametrit `{"input": {...}}`-rakenteessa +- TAI muuta työkalun signatuuri käyttämään suoria parametreja (ei Pydantic-mallia) + +### Opittu + +MCP-protokolla on vielä nuori ja käytännöt vaihtelevat. Testaa aina protokollatasolla ennen kuin syytät logiikkaa. + +--- + +## 2. Sgraph-kirjaston API-erot + +### Ongelma + +Sgraph-kirjaston dokumentaatio ja esimerkit käyttävät eri metodinimiä kuin toteutus: + +| Oletettu | Todellinen | +|----------|------------| +| `SGraph.load()` | Ei ole | +| `SGraph.parse_xml()` | Ei ole | +| `model.root` | `model.rootNode` | + +### Tutkimuspolku + +1. Yritettiin `SGraph.load("file.xml")` → AttributeError +2. Yritettiin `SGraph.parse_xml("file.xml")` → AttributeError +3. Käytettiin `dir(SGraph)` selvittämään oikeat metodit +4. Löydettiin `SGraph.parse_xml_or_zipped_xml()` + +### Opittu + +Kun kirjaston dokumentaatio on epäselvä: +```python +# Listaa kaikki metodit +for name in dir(SGraph): + if not name.startswith('_'): + print(name) +``` + +--- + +## 3. Async/Await -kontekstit + +### Ongelma + +ModelManager.load_model() on async-metodi, mutta tätä ei ole merkitty selvästi: + +```python +model_id = mm.load_model(path) # Palauttaa coroutine, ei model_id:tä! +``` + +Virheilmoitus: +``` +RuntimeWarning: coroutine 'ModelManager.load_model' was never awaited +``` + +### Korjaus + +```python +model_id = await mm.load_model(path) +``` + +### Opittu + +Python-koodissa async-funktiot pitää tunnistaa: +- Tarkista `async def` määrittelyssä +- Jos palautusarvo on ``, tarvitaan await + +--- + +## 4. Mallin hierarkian ymmärtäminen + +### Ongelma + +Softagram-malli käyttää hierarkkista polkurakennetta: + +``` +/TalenomSoftware/Online/talenom.online.invoicepayment5.api/Talenom.Online.InvoicePayment.Api/Controllers +``` + +Mutta: +- Root-elementillä on tyhjä nimi (`""`) +- Ensimmäinen oikea solmu on `/TalenomSoftware` +- Polut eivät vastaa suoraan tiedostojärjestelmän polkuja + +### Tutkimuspolku + +```python +root = graph.rootNode +print(f"Root name: '{root.name}'") # Tyhjä! +print(f"Children: {[c.name for c in root.children]}") # ['TalenomSoftware'] +``` + +### Opittu + +Älä oleta mallin rakennetta - tutki se ensin: +```bash +python3 scripts/structure.py / --depth 2 +``` + +--- + +## 5. Riippuvuustietojen puutteet + +### Ongelma + +Impact-analyysi näytti 0 riippuvuutta vaikka niitä piti olla. (Aiemmassa JSON-formaatissa tämä näkyi kenttänä `incoming_count: 0`, nykyisessä plain text -formaatissa `incoming (0): (none)`.) + +### Syy + +Softagram-malli sisältää eri tarkkuustasoja: +- **Tiedostotaso**: NuGet-paketit, projektireferenssit → mukana +- **Luokkataso**: using-lauseet → mukana +- **Funktiotaso**: metodikutsut → ei aina mukana +- **NuGet-pakettien sisäiset**: ei näy ollenkaan + +### Mitä malli sisältää + +```python +# Elementin riippuvuudet +for assoc in element.outgoing: + print(f"{element.name} -> {assoc.toElement.name} ({assoc.deptype})") +``` + +Tyypilliset `deptype`-arvot: +- `copy`, `from`, `apt` (Docker) +- `import`, `uses` (koodi) +- `project_reference`, `package_reference` (.NET) + +### Opittu + +Sgraph-malli ei ole kaikkitietävä - se näyttää sen mitä Softagram-analyzer on kerännyt. Tarkista ensin mitä riippuvuuksia on olemassa: + +```bash +python3 scripts/deps.py /path/to/element --direction both --json +``` + +--- + +## 6. Serverin käynnistysmoodi + +### Ongelma + +Serveri käynnistyi SSE-modessa kun odotettiin stdio:ta: + +``` +INFO: Uvicorn running on http://0.0.0.0:8008 +``` + +### Syy + +Oletusmoodi on SSE, stdio vaatii eksplisiittisen lipun. + +### Korjaus + +```bash +uv run python -m src.server --profile claude-code --transport stdio +``` + +### MCP-konfiguraatiossa + +```json +{ + "args": ["run", "python", "-m", "src.server", "--profile", "claude-code", "--transport", "stdio"] +} +``` + +--- + +## 7. Skriptien vs. kertakäyttökoodin ero + +### Ongelma + +Debugatessa tuotettiin paljon kertakäyttöistä Python-koodia heredoc-blokeissa, mikä: +- Täyttää kontekstin +- Ei ole uudelleenkäytettävää +- Vaikea lukea + +### Ratkaisu + +Luotiin uudelleenkäytettävät skriptit: +- `sgraph_client.py` - yhteinen kirjasto +- `search.py`, `structure.py`, `deps.py`, `impact.py` - CLI-työkalut + +### Opittu + +Kun debuggaat uutta integraatiota: +1. **Ensimmäinen iteraatio**: Kertakäyttökoodi ok tutkimiseen +2. **Kun ymmärrät rakenteen**: Tee skriptit +3. **Dokumentoi**: Kirjoita troubleshooting-guide + +--- + +## Aikajana + +| Vaihe | Aika | Ongelma | +|-------|------|---------| +| 1 | 0-10 min | Serverin käynnistys, SSE vs stdio | +| 2 | 10-20 min | Sgraph API:n selvittäminen | +| 3 | 20-30 min | Mallin lataus, async-ongelmat | +| 4 | 30-45 min | Hakutulokset tyhjiä - parametrirakenne | +| 5 | 45-55 min | MCP input-wrapper löytyi | +| 6 | 55-65 min | Toimivuuden varmistus | +| 7 | 65-80 min | Skriptien luonti | + +**Kokonaisaika:** ~80 minuuttia + +**Suurin ajansyöppö:** Parametrirakenteen selvittäminen (~25 min) + +--- + +## Suositukset tulevaisuuteen + +### 1. MCP-integraatioiden testaus + +Luo aina testiskripti joka: +```python +async with stdio_client(params) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + tools = await session.list_tools() + print(tools) # Varmista että työkalut näkyvät +``` + +### 2. Parametrien rakenne + +Jos työkalu ei toimi, testaa molemmat: +```python +{"param": "value"} # Suora +{"input": {"param": "value"}} # Wrapattuna +``` + +### 3. Dokumentoi heti + +Kun löydät ratkaisun, kirjoita se ylös ennen kuin unohdat miksi se toimi. + +--- + +## Lopputulos + +Sgraph MCP toimii nyt: +- ✅ Malli latautuu (~20s, 376 MB) +- ✅ Haku toimii (TOON-muoto) +- ✅ Rakenne-navigointi toimii +- ✅ Riippuvuuskyselyt toimivat +- ⚠️ Impact-analyysi rajoitettu (mallin tarkkuustaso) + +Skriptit käytettävissä: `/mnt/c/code/sgraph-mcp-server/scripts/` diff --git a/docs/SGRAPH_MCP_TROUBLESHOOTING.md b/docs/SGRAPH_MCP_TROUBLESHOOTING.md new file mode 100644 index 0000000..a3bbc98 --- /dev/null +++ b/docs/SGRAPH_MCP_TROUBLESHOOTING.md @@ -0,0 +1,170 @@ +# Sgraph MCP Troubleshooting Guide + +Käytännön vinkit ja ratkaisut yleisiin ongelmiin. + +## Pikaohje: Toimivuuden tarkistus + +```bash +cd /mnt/c/code/sgraph-mcp-server +source .venv/bin/activate + +# Testaa suoraan +python3 scripts/search.py CompanyIdentity --max 5 + +# Jos toimii, näet tuloksia ~25 sekunnin kuluttua (mallin lataus) +``` + +## Yleisimmät ongelmat + +### 1. "Field required: input" + +**Oire:** +``` +Error executing tool sgraph_load_model: 1 validation error for sgraph_load_modelArguments +input + Field required [type=missing, input_value={'path': '...'}, ...] +``` + +**Syy:** MCP-protokolla vaatii parametrit `input`-avaimen alle. + +**Ratkaisu:** Parametrit pitää lähettää näin: +```python +# VÄÄRIN +await session.call_tool("sgraph_load_model", {"path": "/path/to/model.xml"}) + +# OIKEIN +await session.call_tool("sgraph_load_model", {"input": {"path": "/path/to/model.xml"}}) +``` + +**Huom:** Claude Code MCP-integraatio saattaa hoitaa tämän automaattisesti - tarkista serverin logit. + +--- + +### 2. Tyhjät hakutulokset + +**Oire:** Haku palauttaa `0/0 matches` (tai legacy-profiilissa `{"results": [], "count": 0}`) + +**Mahdolliset syyt:** + +1. **Model_id puuttuu tai on väärä** + - Varmista että malli on ladattu ja model_id on oikea + - Model_id on session-kohtainen + +2. **Hakupattern ei täsmää** + - Käytä regex-syntaksia: `.*Manager.*` ei `*Manager*` + - Case-insensitive: `(?i)invoice` + +3. **Scope_path virheellinen** + - Polun pitää olla täsmälleen oikein + - Tarkista polku ensin: `scripts/structure.py /TalenomSoftware` + +--- + +### 3. Serveri ei vastaa + +**Oire:** Timeout tai ei yhteyttä + +**Tarkistukset:** + +```bash +# SSE-serverin tila +curl -s http://localhost:8008/health + +# Onko prosessi käynnissä +ps aux | grep "sgraph-mcp-server" | grep -v grep + +# Tapa ja käynnistä uudelleen +pkill -f "sgraph-mcp-server" +cd /mnt/c/code/sgraph-mcp-server +uv run python -m src.server --profile claude-code +``` + +--- + +### 4. Mallin lataus epäonnistuu + +**Oire:** "File not found" tai timeout + +**Tarkistukset:** + +```bash +# Tiedosto olemassa? +ls -la /mnt/c/code/dependency-and-configuration-analyzer/analysis_model.xml + +# Tiedoston koko (pitäisi olla ~376 MB) +du -h /mnt/c/code/dependency-and-configuration-analyzer/analysis_model.xml +``` + +**Latausaika:** Normaali latausaika on 20-25 sekuntia 376 MB mallille. + +--- + +### 5. Impact-analyysi näyttää 0 riippuvuutta + +**Oire:** `incoming (0): (none)` vaikka riippuvuuksia pitäisi olla (legacy-profiilissa `incoming_count: 0`) + +**Syy:** Softagram-malli ei välttämättä sisällä kaikkia riippuvuustasoja: +- Tiedostotason riippuvuudet (import/using) - yleensä mukana +- Funktiotason riippuvuudet (kutsut) - vaatii syvemmän analyysin +- NuGet-pakettien sisäiset riippuvuudet - ei näy + +**Ratkaisu:** Käytä `deps.py` ja tarkista eri tasoilla: +```bash +python3 scripts/deps.py /path/to/element --direction incoming --level 4 +python3 scripts/deps.py /path/to/element --direction incoming # raw level +``` + +--- + +## Hyödylliset polut + +| Resurssi | Polku | +|----------|-------| +| Sgraph MCP Server | `/mnt/c/code/sgraph-mcp-server` | +| Analyysimalli | `/mnt/c/code/dependency-and-configuration-analyzer/analysis_model.xml` | +| Skriptit | `/mnt/c/code/sgraph-mcp-server/scripts/` | +| InvoicePayment API | `/TalenomSoftware/Online/talenom.online.invoicepayment5.api` | +| Talenom.Utilities | `/TalenomSoftware/Talenom.Utilities/talenom.utils.dotnet.core` | + +--- + +## Skriptien käyttö + +```bash +cd /mnt/c/code/sgraph-mcp-server +source .venv/bin/activate + +# Hae elementtejä +python3 scripts/search.py ".*Service.*" --type class --max 20 + +# Näytä rakenne +python3 scripts/structure.py /TalenomSoftware/Online --depth 1 + +# Riippuvuudet +python3 scripts/deps.py /path/to/element --direction outgoing + +# Vaikutusanalyysi +python3 scripts/impact.py /path/to/element +``` + +--- + +## MCP-konfiguraatio + +Tiedosto: `/mnt/c/code/talenom.online.invoicepayment5.api/.mcp.json` + +```json +{ + "mcpServers": { + "sgraph": { + "command": "uv", + "args": ["run", "--project", "/mnt/c/code/sgraph-mcp-server", + "python", "-m", "src.server", + "--profile", "claude-code", "--transport", "stdio"], + "cwd": "/mnt/c/code/sgraph-mcp-server" + } + } +} +``` + +**Huom:** `--transport stdio` on kriittinen Claude Code -integraatiolle. diff --git a/src/core/model_manager.py b/src/core/model_manager.py index 1d26cf4..9124d76 100644 --- a/src/core/model_manager.py +++ b/src/core/model_manager.py @@ -19,10 +19,13 @@ class ModelManager: """Manages sgraph model loading and caching.""" - + def __init__(self): self._models: Dict[str, SGraph] = {} + self._model_paths: Dict[str, str] = {} # model_id -> path self._loader = ModelLoader() + self._default_model_id: Optional[str] = None + self.default_scope: Optional[str] = None logger.info("🔧 ModelManager initialized") async def load_model(self, path: str) -> str: @@ -58,6 +61,7 @@ async def load_model(self, path: str) -> str: # Store model in memory cache self._models[model_id] = model + self._model_paths[model_id] = os.path.abspath(path) logger.info(f"💾 Model cached in memory (total models: {len(self._models)})") # Log basic model info @@ -78,6 +82,41 @@ async def load_model(self, path: str) -> str: logger.error(f"💥 {error_msg}") raise RuntimeError(error_msg) from e + def load_model_sync(self, path: str) -> str: + """Load a model synchronously (for startup auto-load).""" + logger.info(f"🔍 Sync-loading model from: {path}") + + if not os.path.exists(path): + raise FileNotFoundError(f"Model file does not exist: {path}") + + # Check if already loaded from this path + abs_path = os.path.abspath(path) + for mid, mpath in self._model_paths.items(): + if mpath == abs_path: + logger.info(f"♻️ Model already loaded from {path}, reusing ID: {mid}") + return mid + + file_size = os.path.getsize(path) + logger.info(f"📁 File size: {file_size / (1024*1024):.1f} MB") + + start_time = time.perf_counter() + model = self._loader.load_model(path) + load_time = time.perf_counter() - start_time + logger.info(f"✅ Model loaded in {load_time:.2f}s") + + model_id = nanoid.generate(size=24) + self._models[model_id] = model + self._model_paths[model_id] = abs_path + self._default_model_id = model_id + logger.info(f"🆔 Model ID: {model_id} (set as default)") + + return model_id + + @property + def default_model_id(self) -> Optional[str]: + """Get the default model ID (set by auto-load or first loaded model).""" + return self._default_model_id + def get_model(self, model_id: str) -> Optional[SGraph]: """Retrieve a cached model by ID.""" return self._models.get(model_id) diff --git a/src/profiles/base.py b/src/profiles/base.py index 2d2c3a7..dc30ccf 100644 --- a/src/profiles/base.py +++ b/src/profiles/base.py @@ -33,13 +33,23 @@ def register_load_model(mcp: FastMCP) -> None: @mcp.tool() async def sgraph_load_model(input: LoadModelInput): - """Load a graph model from file and return its ID for subsequent queries.""" + """Load a graph model from file and return its ID for subsequent queries. + If the model was already auto-loaded at startup, returns the existing ID instantly.""" try: + model_manager = get_model_manager() + + # Return existing default model if already loaded + if model_manager.default_model_id: + return { + "model_id": model_manager.default_model_id, + "cached": True, + "default_scope": model_manager.default_scope, + } + is_valid, error = validate_path(input.path, must_exist=True) if not is_valid: return {"error": f"Invalid path: {error}"} - model_manager = get_model_manager() model_id = await model_manager.load_model(input.path) return {"model_id": model_id} diff --git a/src/profiles/claude_code.py b/src/profiles/claude_code.py index baa741c..0a409d7 100644 --- a/src/profiles/claude_code.py +++ b/src/profiles/claude_code.py @@ -1,10 +1,7 @@ """ Claude Code profile - optimized for AI-assisted software development. -Aligned with CC_synthesis.md specification. Provides 4 consolidated tools -designed for Claude Code's context constraints: - -Tools (per CC_synthesis.md): +Tools: - sgraph_load_model: Load graph file (shared) - sgraph_search_elements: Find elements by name within scope - sgraph_get_element_dependencies: Dependencies with result_level abstraction @@ -14,8 +11,8 @@ Design principles: - Paths as first-class citizens (unambiguous element identification) - Abstraction as query parameter (result_level: function/file/module) -- Progressive disclosure (5 tools vs 13+, 60% token reduction) -- TOON output format (line-oriented, 50-60% token savings) +- Progressive disclosure (5 tools vs 13+) +- Plain text TOON output (line-oriented, no JSON wrappers) """ from typing import Optional, Literal @@ -30,42 +27,91 @@ # ============================================================================= -# TOON Output Format Utilities +# TOON Output Helpers # ============================================================================= -# TOON (Token-Optimized Object Notation) reduces token consumption by 50-60% -# compared to verbose JSON. Line-oriented format for large datasets. -# -# Example: -# JSON: {"source":"src/A.ts","target":"src/B.ts","type":"import"} (~45 tokens) -# TOON: src/A.ts -> src/B.ts (import) (~15 tokens) -def format_dependency_toon(from_path: str, to_path: str, dep_type: str = "") -> str: - """Format a single dependency as TOON line.""" - if dep_type: - return f"{from_path} -> {to_path} ({dep_type})" - return f"{from_path} -> {to_path}" +def _format_structure(elem, current_depth: int, max_depth: int, lines: list[str]) -> None: + """Append indented TOON lines for element hierarchy.""" + indent = " " * current_depth + etype = elem.getType() or "element" + lines.append(f"{indent}{elem.getPath()} [{etype}] {elem.name}") + + if current_depth < max_depth and elem.children: + for child in elem.children: + _format_structure(child, current_depth + 1, max_depth, lines) -def format_element_toon(path: str, element_type: str, name: str = "") -> str: - """Format a single element as TOON line.""" - if name: - return f"{path} [{element_type}] {name}" - return f"{path} [{element_type}]" +def _collect_deps(element, base_path: str, direction: str, result_level, include_descendants: bool) -> list[str]: + """Collect dependency lines for an element, optionally including descendants. + + For outgoing: "-> /target (type)" or "RelativeChild -> /target (type)" + For incoming: "/source (type) ->" or "/source (type) -> RelativeChild" + Relative paths (no leading /) identify which descendant has the dependency. + """ + lines = [] + seen = set() + + def aggregate(path: str) -> str: + if result_level is None: + return path + parts = path.split("/") + return "/".join(parts[:result_level + 1]) if len(parts) > result_level else path + + def collect_for_element(elem): + # Compute relative path from base element (empty string for self) + elem_path = elem.getPath() + if elem_path == base_path: + relative = "" + else: + relative = elem_path[len(base_path) + 1:] # strip base_path + "/" + + if direction in ("outgoing", "both"): + for assoc in elem.outgoing: + target = aggregate(assoc.toElement.getPath()) + dep_type = getattr(assoc, 'type', '') + key = ("out", relative, target, dep_type) + if key not in seen: + seen.add(key) + type_suffix = f" ({dep_type})" if dep_type else "" + if relative: + lines.append(f"{relative} -> {target}{type_suffix}") + else: + lines.append(f"-> {target}{type_suffix}") + + if direction in ("incoming", "both"): + for assoc in elem.incoming: + source = aggregate(assoc.fromElement.getPath()) + dep_type = getattr(assoc, 'type', '') + key = ("in", source, relative, dep_type) + if key not in seen: + seen.add(key) + type_suffix = f" ({dep_type})" if dep_type else "" + if relative: + lines.append(f"{source}{type_suffix} -> {relative}") + else: + lines.append(f"{source}{type_suffix} ->") + + if include_descendants: + for child in elem.children: + collect_for_element(child) + + collect_for_element(element) + return lines # ============================================================================= -# Input Schemas (aligned with CC_synthesis.md) +# Input Schemas # ============================================================================= class SearchElementsInput(BaseModel): - """Input for sgraph_search_elements (CC_synthesis.md lines 275-291).""" - model_id: str - query: str = Field(description="Name pattern (supports wildcards like 'validate*')") + """Input for sgraph_search_elements.""" + model_id: Optional[str] = Field(default=None, description="Model ID (omit to use auto-loaded default)") + query: str = Field(description="Name pattern (supports wildcards like '*Service*' or regex like '.*Service.*')") scope_path: Optional[str] = Field( default=None, - description="Limit search to subtree (e.g., '/project/src/auth')" + description="Limit search to subtree. Uses server's default scope if configured and not specified." ) element_types: Optional[list[str]] = Field( default=None, @@ -75,12 +121,8 @@ class SearchElementsInput(BaseModel): class GetElementDependenciesInput(BaseModel): - """Input for sgraph_get_element_dependencies (CC_synthesis.md lines 171-218). - - The result_level parameter is THE KEY FEATURE - enables querying same - underlying data at different abstraction levels. - """ - model_id: str + """Input for sgraph_get_element_dependencies.""" + model_id: Optional[str] = Field(default=None, description="Model ID (omit to use auto-loaded default)") element_path: str = Field(description="Full hierarchical path to element") direction: Literal["incoming", "outgoing", "both"] = Field( default="both", @@ -90,12 +132,12 @@ class GetElementDependenciesInput(BaseModel): default=None, description=( "Aggregate results to hierarchy depth. " - "None=raw (as captured), 3=class, 2=file, 1=module/directory" + "None=raw (as captured), 4=file, 3=directory, 2=repository" ) ) - max_depth: Optional[int] = Field( - default=None, - description="Transitive dependency traversal depth" + include_descendants: bool = Field( + default=False, + description="Include dependencies of child elements. Relative paths (no leading /) show which descendant." ) include_external: bool = Field( default=True, @@ -104,18 +146,112 @@ class GetElementDependenciesInput(BaseModel): class GetElementStructureInput(BaseModel): - """Input for sgraph_get_element_structure (CC_synthesis.md lines 222-244).""" - model_id: str + """Input for sgraph_get_element_structure.""" + model_id: Optional[str] = Field(default=None, description="Model ID (omit to use auto-loaded default)") element_path: str = Field(description="Starting point path") max_depth: int = Field(default=2, description="How deep to traverse children") class AnalyzeChangeImpactInput(BaseModel): - """Input for sgraph_analyze_change_impact (CC_synthesis.md lines 248-271).""" - model_id: str + """Input for sgraph_analyze_change_impact.""" + model_id: Optional[str] = Field(default=None, description="Model ID (omit to use auto-loaded default)") element_path: str = Field(description="Element being modified") +class ResolveLocalPathInput(BaseModel): + """Input for sgraph_resolve_local_path - map sgraph paths to local filesystem.""" + sgraph_path: str = Field(description="Sgraph element path (e.g., /TalenomSoftware/Online/repo/file.cs)") + + +# ============================================================================= +# Path Resolution (sgraph -> local filesystem) +# ============================================================================= + +import json +import os +from pathlib import Path + +_path_resolver_config = None + + +def _load_path_config(): + """Load path mapping configuration.""" + global _path_resolver_config + if _path_resolver_config is not None: + return _path_resolver_config + + config = {"mappings": [], "fallback_roots": ["/mnt/c/code/"], "repo_overrides": {}} + + # Search for config file + search_paths = [ + Path(__file__).parent.parent.parent / "sgraph-mapping.json", + Path.cwd() / "sgraph-mapping.json", + Path.home() / ".config" / "sgraph-mapping.json", + ] + + for p in search_paths: + if p.exists(): + with open(p, 'r') as f: + config = json.load(f) + break + + _path_resolver_config = config + return config + + +def _resolve_sgraph_path(sgraph_path: str) -> dict: + """Resolve sgraph path to local filesystem path.""" + config = _load_path_config() + parts = sgraph_path.strip('/').split('/') + + result = { + "sgraph_path": sgraph_path, + "repo_name": parts[2] if len(parts) > 2 else None, + "local_path": None, + "exists": False, + "resolved_via": "not_found" + } + + if len(parts) < 3: + return result + + repo_name = parts[2] + repo_name = config.get("repo_name_overrides", {}).get(repo_name, repo_name) + result["repo_name"] = repo_name + + # Try mappings + for mapping in config.get("mappings", []): + prefix = mapping.get("sgraph_prefix", "/") + if sgraph_path.startswith(prefix): + local_root = mapping.get("local_root", "") + strip_levels = mapping.get("strip_levels", 2) + + remaining_parts = parts[strip_levels + 1:] if len(parts) > strip_levels + 1 else [] + local_path = os.path.join(local_root, repo_name, *remaining_parts) + + if os.path.exists(local_path): + result["local_path"] = local_path + result["exists"] = True + result["resolved_via"] = "mapping" + return result + + if result["local_path"] is None: + result["local_path"] = local_path + + # Try fallback roots + remaining_parts = parts[3:] if len(parts) > 3 else [] + for root in config.get("fallback_roots", []): + root = os.path.expanduser(root) + local_path = os.path.join(root, repo_name, *remaining_parts) + if os.path.exists(local_path): + result["local_path"] = local_path + result["exists"] = True + result["resolved_via"] = "fallback" + return result + + return result + + # ============================================================================= # Profile Implementation # ============================================================================= @@ -123,17 +259,10 @@ class AnalyzeChangeImpactInput(BaseModel): @register_profile("claude-code") class ClaudeCodeProfile: - """Profile optimized for Claude Code IDE integration. - - Implements CC_synthesis.md specification exactly: - - sgraph_search_elements: Find elements by name within scope - - sgraph_get_element_dependencies: THE KEY TOOL with result_level abstraction - - sgraph_get_element_structure: Hierarchy navigation - - sgraph_analyze_change_impact: Multi-level impact for change planning - """ + """Profile optimized for Claude Code IDE integration.""" name = "claude-code" - description = "Optimized for Claude Code - CC_synthesis.md aligned" + description = "Optimized for Claude Code - plain text TOON output" def register_tools(self, mcp: FastMCP) -> None: """Register Claude Code-optimized tools with the MCP server.""" @@ -146,44 +275,44 @@ async def sgraph_search_elements(input: SearchElementsInput): When to use: - You know a class/function name but not its file location - - You want to find all implementations of a pattern (e.g., "*Service", "*Handler") + - You want to find all implementations of a pattern (e.g., "*Service*", "*Handler") - You need to locate a symbol before querying its dependencies Parameters: - - query: Regex pattern (e.g., ".*Manager.*", "validate.*", "^test_") - - scope_path: Limit to subtree (e.g., "/project/src/auth") - faster, fewer results + - query: Wildcards ("*Service*") or regex (".*Service.*") or substring ("Service") + - scope_path: Limit to subtree - faster, fewer results. Auto-set if server has default scope. - element_types: Filter by ["class", "function", "method", "file", "dir"] + - model_id: Omit to use auto-loaded model - Returns TOON format: /path/to/element [type] name + Returns plain text, one element per line: /path/to/element [type] name + First line shows match count: "N/M matches" (shown/total). """ - model = model_manager.get_model(input.model_id) + mid = input.model_id or model_manager.default_model_id + if not mid: + return {"error": "No model loaded. Call sgraph_load_model first."} + model = model_manager.get_model(mid) if model is None: - return {"error": "Model not loaded"} + return {"error": f"Model '{mid}' not found"} + + scope = input.scope_path or model_manager.default_scope try: - # Use existing SearchService elements = SearchService.search_elements_by_name( model, input.query, element_type=input.element_types[0] if input.element_types else None, - scope_path=input.scope_path, + scope_path=scope, ) - # Limit and format as TOON limited = elements[:input.max_results] - toon_lines = [ - format_element_toon(e.getPath(), e.getType() or "element", e.name) - for e in limited - ] + lines = [f"{len(limited)}/{len(elements)} matches"] + for e in limited: + etype = e.getType() or "element" + lines.append(f"{e.getPath()} [{etype}] {e.name}") - return { - "results": toon_lines, - "count": len(limited), - "total_matches": len(elements), - "format": "TOON", - } + return "\n".join(lines) except Exception as e: - return {"error": f"Search failed: {e}"} + return f"error: Search failed: {e}" @mcp.tool() async def sgraph_get_element_dependencies(input: GetElementDependenciesInput): @@ -199,82 +328,52 @@ async def sgraph_get_element_dependencies(input: GetElementDependenciesInput): - "outgoing": What THIS element uses (callees, imports) - for understanding context - "both": Both directions in one call - result_level (THE KEY FEATURE - controls abstraction): - - None: Raw dependencies (function→function) - for precise call sites + result_level (controls abstraction): + - None: Raw dependencies (function->function) - for precise call sites - 4: File level - "which files depend on this?" - 3: Directory level - "which directories depend on this?" - 2: Repository level - "which repos depend on this?" - Example: SElement class with 41 raw deps → 2 unique at repo level + include_descendants: + - false (default): Only this element's own dependencies + - true: Also include children's dependencies. Relative paths (no leading /) + show which descendant: "MyClass/Save -> /target (call)" - Returns TOON format: /from/path -> /to/path (type) + Returns plain text. Outgoing: "-> /target (type)". Incoming: "/source (type) ->". """ - model = model_manager.get_model(input.model_id) + mid = input.model_id or model_manager.default_model_id + if not mid: + return {"error": "No model loaded. Call sgraph_load_model first."} + model = model_manager.get_model(mid) if model is None: - return {"error": "Model not loaded"} + return {"error": f"Model '{mid}' not found"} element = model.findElementFromPath(input.element_path) if element is None: return {"error": f"Element not found: {input.element_path}"} try: - result = { - "element": input.element_path, - "direction": input.direction, - "result_level": input.result_level, - "format": "TOON", - } - - def aggregate_to_level(path: str, level: Optional[int]) -> str: - """Aggregate path to specified hierarchy level.""" - if level is None: - return path - parts = path.split("/") - # Level 1 = first 2 parts, level 2 = first 3 parts, etc. - return "/".join(parts[:level + 1]) if len(parts) > level else path - - def collect_dependencies(direction: str) -> list[str]: - """Collect and format dependencies for given direction.""" - deps = [] - seen = set() - - if direction == "outgoing": - associations = element.outgoing - for assoc in associations: - target = assoc.toElement.getPath() - aggregated = aggregate_to_level(target, input.result_level) - if aggregated not in seen: - seen.add(aggregated) - dep_type = getattr(assoc, 'type', '') - deps.append(format_dependency_toon( - input.element_path, aggregated, dep_type - )) - else: # incoming - associations = element.incoming - for assoc in associations: - source = assoc.fromElement.getPath() - aggregated = aggregate_to_level(source, input.result_level) - if aggregated not in seen: - seen.add(aggregated) - dep_type = getattr(assoc, 'type', '') - deps.append(format_dependency_toon( - aggregated, input.element_path, dep_type - )) - - return deps + sections = [] if input.direction in ("outgoing", "both"): - result["outgoing"] = collect_dependencies("outgoing") - result["outgoing_count"] = len(result["outgoing"]) + out_lines = _collect_deps( + element, input.element_path, "outgoing", + input.result_level, input.include_descendants, + ) + sections.append(f"outgoing ({len(out_lines)}):") + sections.extend(out_lines) if out_lines else sections.append(" (none)") if input.direction in ("incoming", "both"): - result["incoming"] = collect_dependencies("incoming") - result["incoming_count"] = len(result["incoming"]) - - return result - + in_lines = _collect_deps( + element, input.element_path, "incoming", + input.result_level, input.include_descendants, + ) + sections.append(f"incoming ({len(in_lines)}):") + sections.extend(in_lines) if in_lines else sections.append(" (none)") + + return "\n".join(sections) except Exception as e: - return {"error": f"Dependency query failed: {e}"} + return f"error: Dependency query failed: {e}" @mcp.tool() async def sgraph_get_element_structure(input: GetElementStructureInput): @@ -286,42 +385,31 @@ async def sgraph_get_element_structure(input: GetElementStructureInput): - Understand class methods before diving into implementation max_depth: - - 1: Direct children only (file→classes, dir→files) - - 2: Two levels (file→classes→methods) - usually sufficient + - 1: Direct children only (file->classes, dir->files) + - 2: Two levels (file->classes->methods) - usually sufficient - 3+: Deeper nesting (rarely needed) - Returns nested JSON with path, type, name, children[]. + Returns plain text with indented hierarchy. + Each line: /path/to/element [type] name Much cheaper than Read - use this first to decide what to read. """ - model = model_manager.get_model(input.model_id) + mid = input.model_id or model_manager.default_model_id + if not mid: + return {"error": "No model loaded. Call sgraph_load_model first."} + model = model_manager.get_model(mid) if model is None: - return {"error": "Model not loaded"} + return {"error": f"Model '{mid}' not found"} element = model.findElementFromPath(input.element_path) if element is None: return {"error": f"Element not found: {input.element_path}"} - def build_structure(elem, current_depth: int, max_depth: int) -> dict: - """Recursively build structure up to max_depth.""" - node = { - "path": elem.getPath(), - "type": elem.getType() or "element", - "name": elem.name, - } - - if current_depth < max_depth and elem.children: - node["children"] = [ - build_structure(child, current_depth + 1, max_depth) - for child in elem.children - ] - - return node - try: - structure = build_structure(element, 0, input.max_depth) - return structure + lines = [] + _format_structure(element, 0, input.max_depth, lines) + return "\n".join(lines) except Exception as e: - return {"error": f"Structure query failed: {e}"} + return f"error: Structure query failed: {e}" @mcp.tool() async def sgraph_analyze_change_impact(input: AnalyzeChangeImpactInput): @@ -329,32 +417,29 @@ async def sgraph_analyze_change_impact(input: AnalyzeChangeImpactInput): Returns ALL abstraction levels at once (no need for multiple calls): - detailed: Every function/method that uses this element - - file: Which files would need changes - - module: Which modules/repos are affected + - by_file: Which files would need changes + - by_module: Which modules/repos are affected When to use: - - Before changing function signature → see all call sites - - Before renaming class → see all importers - - Before deleting code → verify nothing depends on it - - Planning large refactoring → understand blast radius - - Example output for SElement class: - incoming_count: 41 (functions calling it) - files_affected: 2 (files to modify) - modules_affected: 2 (repos impacted) + - Before changing function signature -> see all call sites + - Before renaming class -> see all importers + - Before deleting code -> verify nothing depends on it + - Planning large refactoring -> understand blast radius - This is the "measure twice, cut once" tool. + Returns plain text with sections for each aggregation level. """ - model = model_manager.get_model(input.model_id) + mid = input.model_id or model_manager.default_model_id + if not mid: + return {"error": "No model loaded. Call sgraph_load_model first."} + model = model_manager.get_model(mid) if model is None: - return {"error": "Model not loaded"} + return {"error": f"Model '{mid}' not found"} element = model.findElementFromPath(input.element_path) if element is None: return {"error": f"Element not found: {input.element_path}"} try: - # Collect all incoming (what uses this element) detailed = [] files = set() modules = set() @@ -363,33 +448,51 @@ async def sgraph_analyze_change_impact(input: AnalyzeChangeImpactInput): source_path = assoc.fromElement.getPath() detailed.append(source_path) - # Aggregate to file level (first 3 path components typically) parts = source_path.split("/") if len(parts) >= 3: files.add("/".join(parts[:3])) - - # Aggregate to module level (first 2 path components) if len(parts) >= 2: modules.add("/".join(parts[:2])) - return { - "element": input.element_path, - "element_type": element.getType() or "element", + sections = [ + f"impact: {len(detailed)} callers, {len(files)} files, {len(modules)} modules", + "", + f"detailed ({len(detailed)}):", + ] + sections.extend(detailed) if detailed else sections.append(" (none)") + sections.append("") + sections.append(f"by_file ({len(files)}):") + sections.extend(sorted(files)) if files else sections.append(" (none)") + sections.append("") + sections.append(f"by_module ({len(modules)}):") + sections.extend(sorted(modules)) if modules else sections.append(" (none)") + + return "\n".join(sections) + except Exception as e: + return f"error: Impact analysis failed: {e}" - "incoming_by_level": { - "detailed": detailed, - "file": sorted(files), - "module": sorted(modules), - }, + @mcp.tool() + async def sgraph_resolve_local_path(input: ResolveLocalPathInput): + """Map sgraph path to local filesystem path. Use to find source code for NuGet packages. + + When to use: + - You found a class/method in sgraph and need to read its source code + - You want to understand what a NuGet package method does internally + - You need to navigate from dependency analysis to actual code - "summary": { - "incoming_count": len(detailed), - "files_affected": len(files), - "modules_affected": len(modules), - }, + The mapping is configured in sgraph-mapping.json. Default maps: + - /TalenomSoftware///... -> /mnt/c/code//... - "format": "TOON", - } + Returns: + - sgraph_path: Original path + - repo_name: Git repository name (3rd level in hierarchy) + - local_path: Resolved filesystem path + - exists: Whether the file/dir exists locally + After resolving, use the Read tool to view the source code. + """ + try: + result = _resolve_sgraph_path(input.sgraph_path) + return result except Exception as e: - return {"error": f"Impact analysis failed: {e}"} + return {"error": f"Path resolution failed: {e}"} diff --git a/src/server.py b/src/server.py index 93ade75..398a01c 100644 --- a/src/server.py +++ b/src/server.py @@ -56,6 +56,18 @@ def parse_args(): choices=["sse", "stdio"], help="Transport to use (default: sse)", ) + parser.add_argument( + "--auto-load", + type=str, + default=None, + help="Path to model file to load automatically at startup", + ) + parser.add_argument( + "--default-scope", + type=str, + default=None, + help="Default scope path for searches (e.g., '/TalenomSoftware/Online/talenom.online.invoicepayment5.api')", + ) return parser.parse_args() @@ -84,6 +96,24 @@ def main(): print(f"❌ Error: {e}", file=log) return 1 + # Auto-load model in background thread (to avoid blocking MCP protocol startup) + if args.auto_load: + import threading + def _bg_load(): + print(f"📂 Auto-loading model (background): {args.auto_load}", file=log, flush=True) + try: + from src.profiles.base import get_model_manager + mm = get_model_manager() + if args.default_scope: + mm.default_scope = args.default_scope + model_id = mm.load_model_sync(args.auto_load) + print(f"✅ Model ready: {model_id}", file=log, flush=True) + if args.default_scope: + print(f"🔍 Default scope: {args.default_scope}", file=log, flush=True) + except Exception as e: + print(f"⚠️ Auto-load failed: {e}", file=log, flush=True) + threading.Thread(target=_bg_load, daemon=True).start() + # Start the server if args.transport == "sse": print(f"🚀 Starting MCP server on http://0.0.0.0:{args.port}", file=log) diff --git a/src/services/search_service.py b/src/services/search_service.py index b517ff0..30e9534 100644 --- a/src/services/search_service.py +++ b/src/services/search_service.py @@ -39,16 +39,16 @@ def search_elements_by_name( # Validate and compile pattern is_valid, error = validate_pattern(pattern) if not is_valid: - logger.warning(f"Invalid pattern: {error}") - # Fallback to literal search - pattern = re.escape(pattern) - - try: + logger.debug(f"Pattern is not valid regex, trying as glob: {error}") + # Convert glob-style pattern to regex + glob_pattern = re.escape(pattern).replace(r"\*", ".*").replace(r"\?", ".") + try: + regex_pattern = re.compile(glob_pattern) + except re.error: + # Final fallback to literal search + regex_pattern = re.compile(re.escape(pattern)) + else: regex_pattern = re.compile(pattern) - except re.error: - # Fallback to glob-style pattern - glob_pattern = pattern.replace("*", ".*").replace("?", ".") - regex_pattern = re.compile(glob_pattern) # Iterative traversal for performance stack = [start_element]