Skip to content

Commit 3fbef9e

Browse files
refactor(mcp): type graph and parse payloads
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 196d298 commit 3fbef9e

4 files changed

Lines changed: 260 additions & 69 deletions

File tree

internal/mcp/handler_graph.go

Lines changed: 90 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,38 @@ type graphCommInfo struct {
3131
Cohesion float64 `json:"cohesion"`
3232
}
3333

34+
// listFlowsResponse holds the listFlows wire payload.
35+
// @intent preserve the legacy listFlows response shape with typed fields.
36+
type listFlowsResponse struct {
37+
Flows []graphFlowInfo `json:"flows"`
38+
DerivedState map[string]any `json:"derived_state"`
39+
Pagination map[string]any `json:"pagination"`
40+
}
41+
42+
// listCommunitiesResponse holds the listCommunities wire payload.
43+
// @intent preserve the legacy listCommunities response shape with typed fields.
44+
type listCommunitiesResponse struct {
45+
Communities []graphCommInfo `json:"communities"`
46+
DerivedState map[string]any `json:"derived_state"`
47+
Pagination map[string]any `json:"pagination"`
48+
}
49+
50+
// communityMemberSummary is a typed member entry for getCommunity.
51+
// @intent preserve the legacy community member shape with typed fields.
52+
type communityMemberSummary = nodeSummary
53+
54+
// getCommunityResponse holds the getCommunity wire payload.
55+
// @intent preserve the legacy getCommunity response shape with typed fields.
56+
type getCommunityResponse struct {
57+
ID uint `json:"id"`
58+
Label string `json:"label"`
59+
NodeCount int64 `json:"node_count"`
60+
DerivedState map[string]any `json:"derived_state"`
61+
Coverage *float64 `json:"coverage,omitempty"`
62+
Members []communityMemberSummary `json:"members,omitempty"`
63+
MembersPagination map[string]any `json:"members_pagination,omitempty"`
64+
}
65+
3466
// archCommCount is a helper struct for counting community nodes in architecture overview.
3567
// @intent support community node counting in getArchitectureOverview without polluting model.Community.
3668
type archCommCount struct {
@@ -39,6 +71,34 @@ type archCommCount struct {
3971
NodeCount int64
4072
}
4173

74+
// architectureOverviewCommunity is a typed community entry for architecture overview.
75+
// @intent preserve the legacy communities item shape with typed fields.
76+
type architectureOverviewCommunity struct {
77+
ID uint `json:"id"`
78+
Label string `json:"label"`
79+
NodeCount int64 `json:"node_count"`
80+
}
81+
82+
// architectureOverviewCoupling is a typed coupling entry for architecture overview.
83+
// @intent preserve the legacy coupling item shape with typed fields.
84+
type architectureOverviewCoupling struct {
85+
From string `json:"from"`
86+
To string `json:"to"`
87+
EdgeCount int64 `json:"edge_count"`
88+
Strength float64 `json:"strength"`
89+
}
90+
91+
// architectureOverviewResponse holds the architecture overview wire payload.
92+
// @intent preserve the legacy architecture overview response shape with typed fields.
93+
type architectureOverviewResponse struct {
94+
Communities []architectureOverviewCommunity `json:"communities"`
95+
CommunitiesPagination map[string]any `json:"communities_pagination"`
96+
Coupling []architectureOverviewCoupling `json:"coupling"`
97+
CouplingPagination map[string]any `json:"coupling_pagination"`
98+
Warnings []string `json:"warnings"`
99+
DerivedState map[string]any `json:"derived_state"`
100+
}
101+
42102
// communityRow is a helper struct for counting community nodes in listCommunities.
43103
// @intent support community node counting in listCommunities without polluting model.Community.
44104
type communityRow struct {
@@ -119,10 +179,10 @@ func (h *handlers) listFlows(ctx context.Context, request mcp.CallToolRequest) (
119179
}
120180
}
121181

122-
result, err := marshalJSON(map[string]any{
123-
"flows": infos,
124-
"derived_state": derivedStateFlows(),
125-
"pagination": buildPaginationMetadata(limit, offset, len(infos), hasMore),
182+
result, err := marshalJSON(listFlowsResponse{
183+
Flows: infos,
184+
DerivedState: derivedStateFlows(),
185+
Pagination: buildPaginationMetadata(limit, offset, len(infos), hasMore),
126186
})
127187
if err != nil {
128188
return "", trace.Wrap(err, "marshal result")
@@ -192,10 +252,10 @@ func (h *handlers) listCommunities(ctx context.Context, request mcp.CallToolRequ
192252
}
193253
}
194254

195-
result, err := marshalJSON(map[string]any{
196-
"communities": infos,
197-
"derived_state": derivedStateCommunities(),
198-
"pagination": buildPaginationMetadata(limit, offset, len(infos), hasMore),
255+
result, err := marshalJSON(listCommunitiesResponse{
256+
Communities: infos,
257+
DerivedState: derivedStateCommunities(),
258+
Pagination: buildPaginationMetadata(limit, offset, len(infos), hasMore),
199259
})
200260
if err != nil {
201261
return "", trace.Wrap(err, "marshal result")
@@ -247,17 +307,18 @@ func (h *handlers) getCommunity(ctx context.Context, request mcp.CallToolRequest
247307
return "", trace.Wrap(err, "count community members")
248308
}
249309

250-
gcData := map[string]any{
251-
"id": comm.ID,
252-
"label": comm.Label,
253-
"node_count": memberCount,
254-
"derived_state": derivedStateCommunities(),
310+
gcData := getCommunityResponse{
311+
ID: comm.ID,
312+
Label: comm.Label,
313+
NodeCount: memberCount,
314+
DerivedState: derivedStateCommunities(),
255315
}
256316

257317
if h.deps.CoverageAnalyzer != nil {
258318
cc, err := h.deps.CoverageAnalyzer.ByCommunity(ctx, comm.ID)
259319
if err == nil && cc != nil {
260-
gcData["coverage"] = cc.Ratio
320+
coverage := cc.Ratio
321+
gcData.Coverage = &coverage
261322
}
262323
}
263324

@@ -284,12 +345,12 @@ func (h *handlers) getCommunity(ctx context.Context, request mcp.CallToolRequest
284345
nodes = nodes[:memberLimit]
285346
}
286347

287-
members := make([]map[string]any, len(nodes))
348+
members := make([]communityMemberSummary, len(nodes))
288349
for i, n := range nodes {
289-
members[i] = nodeToBasicMap(n)
350+
members[i] = nodeToSummary(n)
290351
}
291-
gcData["members"] = members
292-
gcData["members_pagination"] = buildPaginationMetadata(memberLimit, memberOffset, len(members), hasMore)
352+
gcData.Members = members
353+
gcData.MembersPagination = buildPaginationMetadata(memberLimit, memberOffset, len(members), hasMore)
293354
}
294355

295356
result, err := marshalJSON(gcData)
@@ -355,16 +416,12 @@ func (h *handlers) getArchitectureOverview(ctx context.Context, request mcp.Call
355416
archCCRows = archCCRows[:communityLimit]
356417
}
357418

358-
commInfos := make([]map[string]any, len(archCCRows))
419+
commInfos := make([]architectureOverviewCommunity, len(archCCRows))
359420
for i, c := range archCCRows {
360-
commInfos[i] = map[string]any{
361-
"id": c.ID,
362-
"label": c.Label,
363-
"node_count": c.NodeCount,
364-
}
421+
commInfos[i] = architectureOverviewCommunity{ID: c.ID, Label: c.Label, NodeCount: c.NodeCount}
365422
}
366423

367-
couplingPairs := []map[string]any{}
424+
couplingPairs := []architectureOverviewCoupling{}
368425
warnings := []string{}
369426
if len(archCCRows) == 0 {
370427
warnings = []string{"No communities found. Run community rebuild first."}
@@ -375,12 +432,7 @@ func (h *handlers) getArchitectureOverview(ctx context.Context, request mcp.Call
375432
page, err := h.deps.CouplingAnalyzer.AnalyzePage(ctx, paging.Request{Limit: couplingLimit, Offset: couplingOffset})
376433
if err == nil {
377434
for _, cp := range page.Items {
378-
couplingPairs = append(couplingPairs, map[string]any{
379-
"from": cp.FromCommunity,
380-
"to": cp.ToCommunity,
381-
"edge_count": cp.EdgeCount,
382-
"strength": cp.Strength,
383-
})
435+
couplingPairs = append(couplingPairs, architectureOverviewCoupling{From: cp.FromCommunity, To: cp.ToCommunity, EdgeCount: cp.EdgeCount, Strength: cp.Strength})
384436
if cp.Strength > 0.8 {
385437
warnings = append(warnings, fmt.Sprintf("High coupling between %s and %s (strength: %.2f)", cp.FromCommunity, cp.ToCommunity, cp.Strength))
386438
}
@@ -389,13 +441,13 @@ func (h *handlers) getArchitectureOverview(ctx context.Context, request mcp.Call
389441
}
390442
}
391443

392-
result, err := marshalJSON(map[string]any{
393-
"communities": commInfos,
394-
"communities_pagination": buildPaginationMetadata(communityLimit, communityOffset, len(commInfos), communityHasMore),
395-
"coupling": couplingPairs,
396-
"coupling_pagination": buildPaginationMetadata(couplingLimit, couplingOffset, len(couplingPairs), couplingHasMore),
397-
"warnings": warnings,
398-
"derived_state": derivedStateSummary(),
444+
result, err := marshalJSON(architectureOverviewResponse{
445+
Communities: commInfos,
446+
CommunitiesPagination: buildPaginationMetadata(communityLimit, communityOffset, len(commInfos), communityHasMore),
447+
Coupling: couplingPairs,
448+
CouplingPagination: buildPaginationMetadata(couplingLimit, couplingOffset, len(couplingPairs), couplingHasMore),
449+
Warnings: warnings,
450+
DerivedState: derivedStateSummary(),
399451
})
400452
if err != nil {
401453
return "", trace.Wrap(err, "marshal result")

internal/mcp/handler_parse.go

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,31 @@ import (
2222

2323
var refreshSearchDocuments = service.RefreshSearchDocuments
2424

25+
// @intent serialize build_or_update_graph results with a fixed JSON schema without changing the wire format.
26+
type buildOrUpdateGraphResponse struct {
27+
Status string `json:"status"`
28+
FilesParsed int `json:"files_parsed"`
29+
NodesCreated int `json:"nodes_created"`
30+
EdgesCreated int `json:"edges_created"`
31+
ElapsedMS int64 `json:"elapsed_ms"`
32+
PostprocessPolicy string `json:"postprocess_policy"`
33+
PolicySource string `json:"policy_source"`
34+
FailedSteps []string `json:"failed_steps"`
35+
SkippedSteps []string `json:"skipped_steps"`
36+
}
37+
38+
// @intent serialize run_postprocess results with a fixed JSON schema without changing the wire format.
39+
type runPostprocessResponse struct {
40+
Status string `json:"status"`
41+
FlowsCount int `json:"flows_count"`
42+
CommunitiesCount int `json:"communities_count"`
43+
FTSIndexed int `json:"fts_indexed"`
44+
PostprocessPolicy string `json:"postprocess_policy"`
45+
PolicySource string `json:"policy_source"`
46+
FailedSteps []string `json:"failed_steps"`
47+
SkippedSteps []string `json:"skipped_steps"`
48+
}
49+
2550
// @intent apply per-request parse limits without mutating the shared handler dependency configuration.
2651
func (h *handlers) withParseLimitsFromRequest(request mcp.CallToolRequest) *handlers {
2752
maxFileBytes := int64(request.GetInt("max_file_bytes", int(h.deps.MaxFileBytes)))
@@ -282,16 +307,16 @@ func (h *handlers) buildOrUpdateGraph(ctx context.Context, request mcp.CallToolR
282307
status = "degraded"
283308
}
284309

285-
result := map[string]any{
286-
"status": status,
287-
"files_parsed": fileCount,
288-
"nodes_created": nodeCount,
289-
"edges_created": edgeCount,
290-
"elapsed_ms": elapsed,
291-
"postprocess_policy": postprocessPolicy,
292-
"policy_source": policySource,
293-
"failed_steps": failedSteps,
294-
"skipped_steps": skippedSteps,
310+
result := buildOrUpdateGraphResponse{
311+
Status: status,
312+
FilesParsed: fileCount,
313+
NodesCreated: nodeCount,
314+
EdgesCreated: edgeCount,
315+
ElapsedMS: elapsed,
316+
PostprocessPolicy: postprocessPolicy,
317+
PolicySource: string(policySource),
318+
FailedSteps: failedSteps,
319+
SkippedSteps: skippedSteps,
295320
}
296321
if h.deps.PostprocessPolicy != nil {
297322
if err := h.deps.PostprocessPolicy.RecordRun(ctx, postprocesspolicy.RunRecord{
@@ -441,15 +466,15 @@ func (h *handlers) runPostprocess(ctx context.Context, request mcp.CallToolReque
441466
status = "degraded"
442467
}
443468

444-
result := map[string]any{
445-
"status": status,
446-
"flows_count": flowsCount,
447-
"communities_count": communitiesCount,
448-
"fts_indexed": ftsIndexed,
449-
"postprocess_policy": postprocessPolicy,
450-
"policy_source": policySource,
451-
"failed_steps": failedSteps,
452-
"skipped_steps": skippedSteps,
469+
result := runPostprocessResponse{
470+
Status: status,
471+
FlowsCount: flowsCount,
472+
CommunitiesCount: communitiesCount,
473+
FTSIndexed: ftsIndexed,
474+
PostprocessPolicy: postprocessPolicy,
475+
PolicySource: string(policySource),
476+
FailedSteps: failedSteps,
477+
SkippedSteps: skippedSteps,
453478
}
454479
if h.deps.PostprocessPolicy != nil {
455480
if err := h.deps.PostprocessPolicy.RecordRun(ctx, postprocesspolicy.RunRecord{

internal/mcp/handler_postprocess.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ import (
99
postprocesspolicy "github.com/tae2089/code-context-graph/internal/postprocess/policy"
1010
)
1111

12+
// resetPostprocessPolicyResponse is the typed wire payload for resetPostprocessPolicy.
13+
// @intent preserve a stable confirmation envelope after resetting one postprocess policy tool.
14+
type resetPostprocessPolicyResponse struct {
15+
Status string `json:"status"`
16+
Tool string `json:"tool"`
17+
Reset bool `json:"reset"`
18+
}
19+
1220
// getPostprocessPolicy returns the recorded postprocess policy summary for a namespace and tool.
1321
// @intent expose automatic fail-open versus fail-closed decisions so operators can diagnose degraded postprocess behavior.
1422
// @param request optional tool filter (build_or_update_graph or run_postprocess) and recent_limit for failure history depth.
@@ -30,8 +38,8 @@ func (h *handlers) getPostprocessPolicy(ctx context.Context, request mcp.CallToo
3038
return finalizeToolResult("", err)
3139
}
3240
summary, err := h.deps.PostprocessPolicy.Status(ctx, postprocesspolicy.StatusOptions{
33-
Namespace: requestNamespace(request),
34-
Tool: tool,
41+
Namespace: requestNamespace(request),
42+
Tool: tool,
3543
RecentLimit: recentLimit,
3644
})
3745
if err != nil {
@@ -61,10 +69,6 @@ func (h *handlers) resetPostprocessPolicy(ctx context.Context, request mcp.CallT
6169
if err := h.deps.PostprocessPolicy.Reset(ctx, tool); err != nil {
6270
return nil, err
6371
}
64-
result, err := marshalJSON(map[string]any{
65-
"status": "ok",
66-
"tool": tool,
67-
"reset": true,
68-
})
72+
result, err := marshalJSON(resetPostprocessPolicyResponse{Status: "ok", Tool: tool, Reset: true})
6973
return finalizeToolResult(result, err)
7074
}

0 commit comments

Comments
 (0)