Skip to content

Commit ac7ae0d

Browse files
committed
feat: add get_minimal_context MCP tool for lightweight AI agent entry point
Implements a ~100-token summary tool that returns graph stats, risk analysis, top communities/flows, and task-based tool suggestions in a single call. Inspired by code-review-graph's get_minimal_context. - handler_context.go: 5-stage pipeline (stats, risk, communities, flows, tools) - handler_context_test.go: 5 tests covering empty graph, populated graph, top-3 limiting, git risk analysis, and keyword-based tool suggestions - server.go: register as 29th MCP tool - server_test.go: update tool count assertions (28→29) - docs: update README, guide/mcp-tools.md, CLAUDE.md (28→29)
1 parent eaccc10 commit ac7ae0d

7 files changed

Lines changed: 587 additions & 12 deletions

File tree

.claude/CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
## MCP 서버
66

7-
`.mcp.json`에 등록된 ccg MCP 서버가 28개 도구를 제공합니다:
7+
`.mcp.json`에 등록된 ccg MCP 서버가 29개 도구를 제공합니다:
88
- `parse_project`, `build_or_update_graph`, `run_postprocess`
9-
- `get_node`, `search`, `query_graph`, `list_graph_stats`
9+
- `get_node`, `search`, `query_graph`, `list_graph_stats`, `get_minimal_context`
1010
- `get_impact_radius`, `trace_flow`
1111
- `find_large_functions`, `find_dead_code`
1212
- `detect_changes`, `get_affected_flows`, `list_flows`
@@ -45,7 +45,7 @@ Graceful shutdown: SIGINT/SIGTERM 시 진행 중인 clone/build에 context cance
4545

4646
상세 문서는 `guide/` 디렉토리를 참조하세요:
4747
- [CLI Reference](guide/cli-reference.md) — 전체 명령어, 플래그, 설정 파일
48-
- [MCP Tools](guide/mcp-tools.md)28개 MCP 도구, Skills, AI-Driven Annotation
48+
- [MCP Tools](guide/mcp-tools.md)29개 MCP 도구, Skills, AI-Driven Annotation
4949
- [Annotations](guide/annotations.md) — 어노테이션 태그, 예시, 검색
5050
- [Webhook](guide/webhook.md) — Webhook sync, 브랜치 필터링, HMAC, graceful shutdown
5151
- [Docker](guide/docker.md) — Docker 빌드, MCP 서버, PostgreSQL 배포

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# code-context-graph
22

3-
Local code analysis tool that parses codebases via Tree-sitter into a knowledge graph. Supports 12 languages, 28 MCP tools, and custom annotation search.
3+
Local code analysis tool that parses codebases via Tree-sitter into a knowledge graph. Supports 12 languages, 29 MCP tools, and custom annotation search.
44

55
Inspired by [code-review-graph](https://github.com/tirth8205/code-review-graph) — a Python-based code analysis tool. This project reimplements and extends the concept in Go with multi-DB support, custom annotation system, and MCP integration for AI-powered code understanding.
66

77
## Features
88

99
- **12 languages**: Go, Python, TypeScript, Java, Ruby, JavaScript, C, C++, Rust, Kotlin, PHP, Lua
10-
- **28 MCP tools**: parse, search, impact analysis, flow tracing, dead code detection, file workspace management, and more
10+
- **29 MCP tools**: parse, search, impact analysis, flow tracing, dead code detection, file workspace management, and more
1111
- **Custom annotations**: `@intent`, `@domainRule`, `@sideEffect`, `@mutates`, `@index` — search code by business context ([details](guide/annotations.md))
1212
- **Webhook sync**: GitHub / Gitea push events → auto clone + build with per-repo branch filtering ([details](guide/webhook.md))
1313
- **Multi-DB**: SQLite (local), PostgreSQL
@@ -80,7 +80,7 @@ For remote HTTP mode:
8080
}
8181
```
8282

83-
Claude Code automatically connects and gets access to 28 MCP tools. See [MCP Tools Reference](guide/mcp-tools.md) for the full list.
83+
Claude Code automatically connects and gets access to 29 MCP tools. See [MCP Tools Reference](guide/mcp-tools.md) for the full list.
8484

8585
## Architecture
8686

@@ -91,7 +91,7 @@ Source Code → Tree-sitter Parser → Nodes + Edges + Annotations
9191
9292
FTS Search
9393
94-
MCP Server (28 tools)
94+
MCP Server (29 tools)
9595
↓ ↓
9696
stdio Streamable HTTP
9797
↓ ↓
@@ -108,7 +108,7 @@ See [Architecture Details](guide/architecture.md) for component breakdown and DB
108108
| Guide | Description |
109109
|-------|-------------|
110110
| [CLI Reference](guide/cli-reference.md) | All commands, flags, and config file (`.ccg.yaml`) |
111-
| [MCP Tools](guide/mcp-tools.md) | 28 MCP tools, Skills, AI-Driven Annotation |
111+
| [MCP Tools](guide/mcp-tools.md) | 29 MCP tools, Skills, AI-Driven Annotation |
112112
| [Annotations](guide/annotations.md) | Annotation system — tags, examples, search |
113113
| [Webhook](guide/webhook.md) | Webhook sync, branch filtering, HMAC, graceful shutdown |
114114
| [Docker](guide/docker.md) | Docker build, MCP server, PostgreSQL deployment |

guide/mcp-tools.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# MCP Tools
22

3-
code-context-graph는 28개 MCP 도구를 제공합니다. Claude Code에서 `.mcp.json` 설정 후 자동 연결됩니다.
3+
code-context-graph는 29개 MCP 도구를 제공합니다. Claude Code에서 `.mcp.json` 설정 후 자동 연결됩니다.
44

55
## Setup
66

@@ -30,7 +30,7 @@ code-context-graph는 28개 MCP 도구를 제공합니다. Claude Code에서 `.m
3030
}
3131
```
3232

33-
## Tools (28)
33+
## Tools (29)
3434

3535
### Core
3636

@@ -43,6 +43,7 @@ code-context-graph는 28개 MCP 도구를 제공합니다. Claude Code에서 `.m
4343
| `search` | Full-text search |
4444
| `query_graph` | Predefined graph queries (callers, callees, imports, etc.) |
4545
| `list_graph_stats` | Node/edge/file counts |
46+
| `get_minimal_context` | Lightweight summary (~100 tokens) for AI agent entry point — graph stats, risk, top communities/flows, tool suggestions |
4647

4748
### Analysis
4849

internal/mcp/handler_context.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sort"
7+
"strings"
8+
9+
"github.com/mark3labs/mcp-go/mcp"
10+
"github.com/tae2089/trace"
11+
12+
"github.com/imtaebin/code-context-graph/internal/analysis/changes"
13+
"github.com/imtaebin/code-context-graph/internal/ctxns"
14+
"github.com/imtaebin/code-context-graph/internal/model"
15+
)
16+
17+
func (h *handlers) getMinimalContext(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
18+
ctx = h.applyWorkspace(ctx, request)
19+
log := h.logger()
20+
21+
task := request.GetString("task", "")
22+
repoRoot := request.GetString("repo_root", "")
23+
base := request.GetString("base", "HEAD~1")
24+
25+
log.Info("get_minimal_context called", "task", task, "repo_root", repoRoot, "base", base)
26+
27+
return finalizeToolResult(h.cachedExecute("get_minimal_context:", map[string]any{
28+
"task": task,
29+
"repo_root": repoRoot,
30+
"base": base,
31+
"workspace": request.GetString("workspace", ""),
32+
}, func() (string, error) {
33+
ns := ctxns.FromContext(ctx)
34+
35+
nodeQ := h.deps.DB.WithContext(ctx).Model(&model.Node{})
36+
if ns != "" {
37+
nodeQ = nodeQ.Where("namespace = ?", ns)
38+
}
39+
var nodeCount, edgeCount int64
40+
if err := nodeQ.Count(&nodeCount).Error; err != nil {
41+
return "", trace.Wrap(err, "count nodes")
42+
}
43+
if err := h.deps.DB.WithContext(ctx).Model(&model.Edge{}).Count(&edgeCount).Error; err != nil {
44+
return "", trace.Wrap(err, "count edges")
45+
}
46+
47+
type fileCount struct{ Count int64 }
48+
var fc fileCount
49+
fileQ := h.deps.DB.WithContext(ctx).Model(&model.Node{}).Select("COUNT(DISTINCT file_path) as count")
50+
if ns != "" {
51+
fileQ = fileQ.Where("namespace = ?", ns)
52+
}
53+
if err := fileQ.Scan(&fc).Error; err != nil {
54+
return "", trace.Wrap(err, "count files")
55+
}
56+
57+
summary := fmt.Sprintf("%d nodes, %d edges, %d files", nodeCount, edgeCount, fc.Count)
58+
59+
risk := "unknown"
60+
var riskScore float64
61+
var keyEntities []string
62+
var testGaps int
63+
64+
if repoRoot != "" && h.deps.ChangesGitClient != nil {
65+
chSvc := changes.New(h.deps.DB, h.deps.ChangesGitClient)
66+
risks, err := chSvc.Analyze(ctx, repoRoot, base)
67+
if err == nil && len(risks) > 0 {
68+
var maxRisk float64
69+
var totalRisk float64
70+
for _, r := range risks {
71+
if r.RiskScore > maxRisk {
72+
maxRisk = r.RiskScore
73+
}
74+
totalRisk += r.RiskScore
75+
keyEntities = append(keyEntities, r.Node.QualifiedName)
76+
}
77+
riskScore = totalRisk / float64(len(risks))
78+
79+
switch {
80+
case maxRisk >= 0.7:
81+
risk = "high"
82+
case maxRisk >= 0.4:
83+
risk = "medium"
84+
default:
85+
risk = "low"
86+
}
87+
88+
if len(keyEntities) > 5 {
89+
keyEntities = keyEntities[:5]
90+
}
91+
92+
for _, r := range risks {
93+
hasTest := false
94+
var testEdges int64
95+
h.deps.DB.WithContext(ctx).Model(&model.Edge{}).
96+
Where("to_node_id = ? AND kind = ?", r.Node.ID, model.EdgeKindTestedBy).
97+
Count(&testEdges)
98+
if testEdges > 0 {
99+
hasTest = true
100+
}
101+
if !hasTest {
102+
testGaps++
103+
}
104+
}
105+
}
106+
}
107+
if keyEntities == nil {
108+
keyEntities = []string{}
109+
}
110+
111+
type commCount struct {
112+
CommunityID uint
113+
Count int
114+
}
115+
var ccRows []commCount
116+
if err := h.deps.DB.WithContext(ctx).
117+
Model(&model.CommunityMembership{}).
118+
Select("community_id, COUNT(*) as count").
119+
Group("community_id").
120+
Scan(&ccRows).Error; err != nil {
121+
return "", trace.Wrap(err, "group community memberships")
122+
}
123+
ccMap := make(map[uint]int, len(ccRows))
124+
for _, r := range ccRows {
125+
ccMap[r.CommunityID] = r.Count
126+
}
127+
128+
var communities []model.Community
129+
if err := h.deps.DB.WithContext(ctx).Find(&communities).Error; err != nil {
130+
return "", trace.Wrap(err, "find communities")
131+
}
132+
133+
type commInfo struct {
134+
Label string `json:"label"`
135+
NodeCount int `json:"node_count"`
136+
}
137+
commInfos := make([]commInfo, len(communities))
138+
for i, c := range communities {
139+
commInfos[i] = commInfo{Label: c.Label, NodeCount: ccMap[c.ID]}
140+
}
141+
sort.Slice(commInfos, func(i, j int) bool {
142+
return commInfos[i].NodeCount > commInfos[j].NodeCount
143+
})
144+
if len(commInfos) > 3 {
145+
commInfos = commInfos[:3]
146+
}
147+
148+
type flowCount struct {
149+
FlowID uint
150+
Count int
151+
}
152+
var fcRows []flowCount
153+
if err := h.deps.DB.WithContext(ctx).
154+
Model(&model.FlowMembership{}).
155+
Select("flow_id, COUNT(*) as count").
156+
Group("flow_id").
157+
Scan(&fcRows).Error; err != nil {
158+
return "", trace.Wrap(err, "group flow memberships")
159+
}
160+
fcMap := make(map[uint]int, len(fcRows))
161+
for _, r := range fcRows {
162+
fcMap[r.FlowID] = r.Count
163+
}
164+
165+
var flowList []model.Flow
166+
if err := h.deps.DB.WithContext(ctx).Find(&flowList).Error; err != nil {
167+
return "", trace.Wrap(err, "find flows")
168+
}
169+
170+
type flowInfo struct {
171+
Name string `json:"name"`
172+
NodeCount int `json:"node_count"`
173+
}
174+
flowInfos := make([]flowInfo, len(flowList))
175+
for i, f := range flowList {
176+
flowInfos[i] = flowInfo{Name: f.Name, NodeCount: fcMap[f.ID]}
177+
}
178+
sort.Slice(flowInfos, func(i, j int) bool {
179+
return flowInfos[i].NodeCount > flowInfos[j].NodeCount
180+
})
181+
if len(flowInfos) > 3 {
182+
flowInfos = flowInfos[:3]
183+
}
184+
185+
suggestedTools := suggestTools(task)
186+
187+
resp := map[string]any{
188+
"summary": summary,
189+
"risk": risk,
190+
"risk_score": riskScore,
191+
"key_entities": keyEntities,
192+
"test_gaps": testGaps,
193+
"top_communities": commInfos,
194+
"top_flows": flowInfos,
195+
"suggested_tools": suggestedTools,
196+
}
197+
198+
result, err := marshalJSON(resp)
199+
if err != nil {
200+
return "", trace.Wrap(err, "marshal result")
201+
}
202+
return result, nil
203+
}))
204+
}
205+
206+
func suggestTools(task string) []string {
207+
lower := strings.ToLower(task)
208+
209+
reviewKeywords := []string{"review", "pr", "merge", "diff"}
210+
debugKeywords := []string{"debug", "bug", "error", "fix"}
211+
refactorKeywords := []string{"refactor", "rename", "dead", "clean"}
212+
onboardKeywords := []string{"onboard", "understand", "explore", "arch"}
213+
214+
for _, kw := range reviewKeywords {
215+
if strings.Contains(lower, kw) {
216+
return []string{"detect_changes", "get_affected_flows", "search"}
217+
}
218+
}
219+
for _, kw := range debugKeywords {
220+
if strings.Contains(lower, kw) {
221+
return []string{"search", "query_graph", "trace_flow"}
222+
}
223+
}
224+
for _, kw := range refactorKeywords {
225+
if strings.Contains(lower, kw) {
226+
return []string{"find_dead_code", "find_large_functions", "get_architecture_overview"}
227+
}
228+
}
229+
for _, kw := range onboardKeywords {
230+
if strings.Contains(lower, kw) {
231+
return []string{"get_architecture_overview", "list_communities", "list_flows"}
232+
}
233+
}
234+
235+
return []string{"detect_changes", "search", "get_architecture_overview"}
236+
}

0 commit comments

Comments
 (0)