Skip to content

Commit 196d298

Browse files
refactor(mcp): type shared analysis responses
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent f33ba67 commit 196d298

5 files changed

Lines changed: 132 additions & 34 deletions

File tree

internal/mcp/handler_analysis.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,31 @@ const (
2727
defaultTraceMaxNodes = 200
2828
)
2929

30+
// impactRadiusMetadata records bounded traversal settings and truncation state.
31+
// @intent explain how getImpactRadius constrained the blast-radius traversal for the returned payload.
3032
type impactRadiusMetadata struct {
3133
Truncated bool `json:"truncated"`
3234
MaxDepth int `json:"max_depth"`
3335
MaxNodes int `json:"max_nodes"`
3436
ReturnedNodes int `json:"returned_nodes"`
3537
}
3638

39+
// impactRadiusResponse is the typed wire payload for getImpactRadius.
40+
// @intent preserve a stable typed response envelope for impact-radius queries.
3741
type impactRadiusResponse struct {
38-
Nodes []map[string]any `json:"nodes"`
42+
Nodes []nodeSummary `json:"nodes"`
3943
Metadata impactRadiusMetadata `json:"metadata"`
4044
}
4145

46+
// traceFlowMember captures one ordered member inside a traced flow.
47+
// @intent serialize flow member references without exposing the full node record.
4248
type traceFlowMember struct {
4349
NodeID uint `json:"node_id"`
4450
Ordinal int `json:"ordinal"`
4551
}
4652

53+
// traceFlowMetadata records bounded trace settings and fallback-edge summary data.
54+
// @intent explain whether traceFlow truncated members and whether fallback edges contributed to the result.
4755
type traceFlowMetadata struct {
4856
Truncated bool `json:"truncated"`
4957
MaxNodes int `json:"max_nodes"`
@@ -52,33 +60,43 @@ type traceFlowMetadata struct {
5260
FallbackEdgesCount int `json:"fallback_edges_count"`
5361
}
5462

63+
// traceFlowResponse is the typed wire payload for traceFlow.
64+
// @intent preserve a stable response envelope for traced flow results and their evidence.
5565
type traceFlowResponse struct {
5666
Name string `json:"name"`
5767
Members []traceFlowMember `json:"members"`
5868
Metadata traceFlowMetadata `json:"metadata"`
5969
Evidence map[string]any `json:"evidence"`
6070
}
6171

72+
// detectChangesEntry summarizes one changed node plus its diff-derived risk score.
73+
// @intent preserve a stable per-item DTO for detectChanges pagination results.
6274
type detectChangesEntry struct {
6375
Name string `json:"name"`
6476
File string `json:"file"`
6577
HunkCount int `json:"hunk_count"`
6678
RiskScore float64 `json:"risk_score"`
6779
}
6880

81+
// detectChangesResponse is the typed wire payload for detectChanges.
82+
// @intent expose diff-risk results with both legacy entries and shared pagination fields.
6983
type detectChangesResponse struct {
7084
Base string `json:"base"`
7185
Entries []detectChangesEntry `json:"entries"`
7286
Items []detectChangesEntry `json:"items"`
7387
Pagination paging.Page `json:"pagination"`
7488
}
7589

90+
// affectedFlowEntry summarizes one stored flow touched by changed nodes.
91+
// @intent preserve a stable DTO for getAffectedFlows items while retaining changed node identifiers.
7692
type affectedFlowEntry struct {
7793
ID uint `json:"id"`
7894
Name string `json:"name"`
7995
AffectedNodes []uint `json:"affected_nodes"`
8096
}
8197

98+
// affectedFlowsResponse is the typed wire payload for getAffectedFlows.
99+
// @intent expose affected stored flows with backward-compatible aliases and pagination metadata.
82100
type affectedFlowsResponse struct {
83101
Base string `json:"base"`
84102
AffectedFlows []affectedFlowEntry `json:"affected_flows"`
@@ -87,13 +105,17 @@ type affectedFlowsResponse struct {
87105
Pagination paging.Page `json:"pagination"`
88106
}
89107

108+
// deadCodeItem summarizes one node reported as dead code.
109+
// @intent preserve a stable per-item DTO for findDeadCode responses.
90110
type deadCodeItem struct {
91111
Name string `json:"name"`
92112
Kind model.NodeKind `json:"kind"`
93113
File string `json:"file"`
94114
StartLine int `json:"start_line"`
95115
}
96116

117+
// suspectFallbackEdgeItem summarizes one fallback edge inspected for suspicion.
118+
// @intent preserve a stable per-item DTO for findSuspectFallbackEdges responses.
97119
type suspectFallbackEdgeItem struct {
98120
EdgeKind model.EdgeKind `json:"edge_kind"`
99121
Fingerprint string `json:"fingerprint"`
@@ -168,9 +190,9 @@ func (h *handlers) getImpactRadius(ctx context.Context, request mcp.CallToolRequ
168190

169191
log.InfoContext(ctx, "get_impact_radius completed", append(obs.TraceLogArgs(ctx), "qualified_name", qn, "result_count", len(nodes))...)
170192

171-
impactResult := make([]map[string]any, len(nodes))
193+
impactResult := make([]nodeSummary, len(nodes))
172194
for i, n := range nodes {
173-
impactResult[i] = nodeToBasicMap(n)
195+
impactResult[i] = nodeToSummary(n)
174196
}
175197
result, err := marshalJSON(impactRadiusResponse{
176198
Nodes: impactResult,

internal/mcp/handler_analysis_io.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,21 @@ func normalizeListPaging(limit, offset int) (paging.Request, error) {
100100
return pageReq, nil
101101
}
102102

103+
// pagedListResponse preserves the shared {legacyKey, items, count, pagination} list envelope.
104+
// @intent let analysis handlers reuse one typed pagination DTO while keeping historical alias fields.
103105
type pagedListResponse[T any] struct {
104106
LegacyKey string
105107
Items []T `json:"items"`
106108
Count int `json:"count"`
107109
Pagination paging.Page `json:"pagination"`
108110
}
109111

112+
// MarshalJSON emits both the legacy alias key and the shared paging fields.
113+
// @intent preserve backward-compatible response keys while allowing handlers to work with typed slices.
114+
// @return returns a JSON object containing the legacy alias, items, count, and pagination fields.
110115
func (r pagedListResponse[T]) MarshalJSON() ([]byte, error) {
111116
resp := map[string]any{
112-
r.LegacyKey: r.Items,
117+
r.LegacyKey: r.Items,
113118
"items": r.Items,
114119
"count": r.Count,
115120
"pagination": r.Pagination,

internal/mcp/handler_context.go

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ type minimalContextFlowInfo struct {
5555
NodeCount int `json:"node_count"`
5656
}
5757

58+
// minimalContextResponse is the typed minimal-context payload sent over MCP.
59+
// @intent keep the minimal-context wire shape explicit without changing serialized output.
60+
type minimalContextResponse struct {
61+
Summary string `json:"summary"`
62+
Risk string `json:"risk"`
63+
RiskScore float64 `json:"risk_score"`
64+
KeyEntities []string `json:"key_entities"`
65+
TestGaps int `json:"test_gaps"`
66+
TopCommunities []minimalContextCommInfo `json:"top_communities"`
67+
TopFlows []minimalContextFlowInfo `json:"top_flows"`
68+
DerivedState map[string]any `json:"derived_state"`
69+
SuggestedTools []string `json:"suggested_tools"`
70+
Evidence map[string]any `json:"evidence"`
71+
}
72+
5873
// getMinimalContext returns a compact project snapshot with risk hints and suggested tools.
5974
// @intent give agents a cheap first read of namespace state before they spend tokens on deeper graph queries.
6075
func (h *handlers) getMinimalContext(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
@@ -218,17 +233,17 @@ func (h *handlers) getMinimalContext(ctx context.Context, request mcp.CallToolRe
218233

219234
suggestedTools := suggestTools(task)
220235

221-
resp := map[string]any{
222-
"summary": summary,
223-
"risk": risk,
224-
"risk_score": riskScore,
225-
"key_entities": keyEntities,
226-
"test_gaps": testGaps,
227-
"top_communities": commInfos,
228-
"top_flows": flowInfos,
229-
"derived_state": derivedStateSummary(),
230-
"suggested_tools": suggestedTools,
231-
"evidence": h.workspaceEvidenceFromContext(ctx),
236+
resp := minimalContextResponse{
237+
Summary: summary,
238+
Risk: risk,
239+
RiskScore: riskScore,
240+
KeyEntities: keyEntities,
241+
TestGaps: testGaps,
242+
TopCommunities: commInfos,
243+
TopFlows: flowInfos,
244+
DerivedState: derivedStateSummary(),
245+
SuggestedTools: suggestedTools,
246+
Evidence: h.workspaceEvidenceFromContext(ctx),
232247
}
233248

234249
result, err := marshalJSON(resp)

internal/mcp/handler_query.go

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ const (
2424
maxQueryGraphLimit = 500
2525
)
2626

27+
// largeFunctionItem summarizes one oversized function candidate.
28+
// @intent preserve a stable per-item DTO for findLargeFunctions responses.
2729
type largeFunctionItem struct {
2830
Name string `json:"name"`
2931
File string `json:"file"`
3032
Lines int `json:"lines"`
3133
}
3234

35+
// annotationTagItem serializes one stored annotation tag.
36+
// @intent expose annotation tags with typed fields for getAnnotation callers.
3337
type annotationTagItem struct {
3438
Kind model.TagKind `json:"kind"`
3539
Type string `json:"type"`
@@ -38,18 +42,24 @@ type annotationTagItem struct {
3842
Ordinal int `json:"ordinal"`
3943
}
4044

45+
// annotationResponse is the typed wire payload for getAnnotation.
46+
// @intent preserve a stable response envelope for annotation summary, context, and tags.
4147
type annotationResponse struct {
4248
Summary string `json:"summary"`
4349
Context string `json:"context"`
4450
Tags []annotationTagItem `json:"tags"`
4551
}
4652

53+
// queryGraphEvidence records edge evidence backing one queryGraph result item.
54+
// @intent expose edge location details that justify caller/callee confidence labels.
4755
type queryGraphEvidence struct {
4856
FilePath string `json:"file_path"`
4957
Line int `json:"line"`
5058
Fingerprint string `json:"fingerprint"`
5159
}
5260

61+
// queryGraphResultItem summarizes one node returned by queryGraph.
62+
// @intent preserve a stable DTO for paged graph traversal results.
5363
type queryGraphResultItem struct {
5464
ID uint `json:"id"`
5565
QualifiedName string `json:"qualified_name"`
@@ -61,6 +71,8 @@ type queryGraphResultItem struct {
6171
Evidence *queryGraphEvidence `json:"evidence,omitempty"`
6272
}
6373

74+
// queryGraphMetadata records pagination and fallback-call accounting for queryGraph.
75+
// @intent explain result counts, truncation, and strict-versus-tentative composition in queryGraph responses.
6476
type queryGraphMetadata struct {
6577
Limit int `json:"limit"`
6678
Offset int `json:"offset"`
@@ -73,14 +85,18 @@ type queryGraphMetadata struct {
7385
IncludeFallbackCalls *bool `json:"include_fallback_calls,omitempty"`
7486
}
7587

88+
// queryGraphResponse is the typed wire payload for queryGraph.
89+
// @intent preserve a stable response envelope for predefined graph traversals and their evidence.
7690
type queryGraphResponse struct {
77-
Pattern string `json:"pattern"`
78-
Target string `json:"target"`
91+
Pattern string `json:"pattern"`
92+
Target string `json:"target"`
7993
Results []queryGraphResultItem `json:"results"`
80-
Metadata queryGraphMetadata `json:"metadata"`
81-
Evidence map[string]any `json:"evidence"`
94+
Metadata queryGraphMetadata `json:"metadata"`
95+
Evidence map[string]any `json:"evidence"`
8296
}
8397

98+
// searchResultItem summarizes one node hit returned by full-text search.
99+
// @intent preserve a stable per-item DTO for search responses.
84100
type searchResultItem struct {
85101
ID uint `json:"id"`
86102
QualifiedName string `json:"qualified_name"`
@@ -89,6 +105,16 @@ type searchResultItem struct {
89105
FilePath string `json:"file_path"`
90106
}
91107

108+
// fileSummaryResponse is the typed wire payload for file_summary queryGraph requests.
109+
// @intent preserve a stable response envelope for file-summary graph queries.
110+
type fileSummaryResponse struct {
111+
Pattern string `json:"pattern"`
112+
Target string `json:"target"`
113+
Results *querypkg.FileSummary `json:"results"`
114+
}
115+
116+
// nodeResponse is the typed wire payload for getNode.
117+
// @intent preserve a stable response envelope for node metadata lookups.
92118
type nodeResponse struct {
93119
ID uint `json:"id"`
94120
QualifiedName string `json:"qualified_name"`
@@ -337,12 +363,7 @@ func (h *handlers) queryGraph(ctx context.Context, request mcp.CallToolRequest)
337363
if err != nil {
338364
return "", newToolResultErr(fmt.Sprintf("file summary error: %v", err))
339365
}
340-
fsData := map[string]any{
341-
"pattern": pattern,
342-
"target": target,
343-
"results": summary,
344-
}
345-
result, err := marshalJSON(fsData)
366+
result, err := marshalJSON(fileSummaryResponse{Pattern: pattern, Target: target, Results: summary})
346367
if err != nil {
347368
return "", trace.Wrap(err, "marshal result")
348369
}
@@ -568,6 +589,17 @@ func compactQueryTargetAmbiguity(target string, matches []querypkg.CandidateMatc
568589
return fmt.Sprintf("query_graph target %q is ambiguous: %s", target, strings.Join(parts, "; "))
569590
}
570591

592+
// listGraphStatsResponse is the serialized payload for graph statistics.
593+
// @intent preserve a stable typed JSON response for graph statistics without changing the wire format.
594+
type listGraphStatsResponse struct {
595+
TotalNodes int64 `json:"total_nodes"`
596+
TotalEdges int64 `json:"total_edges"`
597+
NodesByKind map[string]int64 `json:"nodes_by_kind"`
598+
NodesByLanguage map[string]int64 `json:"nodes_by_language"`
599+
EdgesByKind map[string]int64 `json:"edges_by_kind"`
600+
Evidence map[string]any `json:"evidence"`
601+
}
602+
571603
// listGraphStats returns aggregate node and edge statistics for the graph.
572604
// @intent summarize the current graph load state with kind and language distributions.
573605
// @ensures returns total node and edge counts plus kind and language aggregates when the query succeeds.
@@ -634,13 +666,13 @@ func (h *handlers) listGraphStats(ctx context.Context, request mcp.CallToolReque
634666
ebk[k.Kind] = k.Count
635667
}
636668

637-
statsData := map[string]any{
638-
"total_nodes": nodeCount,
639-
"total_edges": edgeCount,
640-
"nodes_by_kind": nbk,
641-
"nodes_by_language": nbl,
642-
"edges_by_kind": ebk,
643-
"evidence": h.workspaceEvidenceFromContext(ctx),
669+
statsData := listGraphStatsResponse{
670+
TotalNodes: nodeCount,
671+
TotalEdges: edgeCount,
672+
NodesByKind: nbk,
673+
NodesByLanguage: nbl,
674+
EdgesByKind: ebk,
675+
Evidence: h.workspaceEvidenceFromContext(ctx),
644676
}
645677
result, err := marshalJSON(statsData)
646678
if err != nil {

internal/mcp/handlers.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,34 @@ func (h *handlers) cachedExecute(ctx context.Context, prefix string, params map[
227227
return result, nil
228228
}
229229

230+
// nodeSummary is a compact node response payload shared by graph handlers.
231+
// @intent reuse one typed node representation across multiple tool responses.
232+
type nodeSummary struct {
233+
ID uint `json:"id"`
234+
QualifiedName string `json:"qualified_name"`
235+
Kind model.NodeKind `json:"kind"`
236+
Name string `json:"name"`
237+
FilePath string `json:"file_path"`
238+
}
239+
240+
// nodeToSummary converts a graph node into a compact typed response payload.
241+
// @intent reuse one typed node representation across multiple tool responses.
242+
// @param n is the graph node to include in an MCP response.
243+
// @return returns a typed node summary containing identifier, name, kind, and file path fields.
244+
func nodeToSummary(n model.Node) nodeSummary {
245+
return nodeSummary{
246+
ID: n.ID,
247+
QualifiedName: n.QualifiedName,
248+
Kind: n.Kind,
249+
Name: n.Name,
250+
FilePath: n.FilePath,
251+
}
252+
}
253+
230254
// nodeToBasicMap converts a graph node into a compact response payload.
231-
// @intent reuse one compact node representation across multiple tool responses.
255+
// @intent preserve the legacy map-based node shape for existing MCP callers.
232256
// @param n is the graph node to include in an MCP response.
233-
// @return returns a map containing identifier, name, kind, and file path fields.
257+
// @return returns a map with identifier, name, kind, and file path fields.
234258
func nodeToBasicMap(n model.Node) map[string]any {
235259
return map[string]any{
236260
"id": n.ID,

0 commit comments

Comments
 (0)