Skip to content

Commit 153e5e6

Browse files
refactor(mcp): use typed pagination metadata
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent ee7a42f commit 153e5e6

3 files changed

Lines changed: 12 additions & 40 deletions

File tree

internal/mcp/handler_analysis_io.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ type pagedListResponse[T any] struct {
111111

112112
// MarshalJSON emits both the legacy alias key and the shared paging fields.
113113
// @intent preserve backward-compatible response keys while allowing handlers to work with typed slices.
114+
// @domainRule the temporary map allocation remains because the legacy alias key is dynamic per handler and must coexist with the shared typed envelope.
114115
// @return returns a JSON object containing the legacy alias, items, count, and pagination fields.
115116
func (r pagedListResponse[T]) MarshalJSON() ([]byte, error) {
116117
resp := map[string]any{

internal/mcp/handler_graph.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ type graphCommInfo struct {
3636
type listFlowsResponse struct {
3737
Flows []graphFlowInfo `json:"flows"`
3838
DerivedState map[string]any `json:"derived_state"`
39-
Pagination map[string]any `json:"pagination"`
39+
Pagination paging.Page `json:"pagination"`
4040
}
4141

4242
// listCommunitiesResponse holds the listCommunities wire payload.
4343
// @intent preserve the legacy listCommunities response shape with typed fields.
4444
type listCommunitiesResponse struct {
4545
Communities []graphCommInfo `json:"communities"`
4646
DerivedState map[string]any `json:"derived_state"`
47-
Pagination map[string]any `json:"pagination"`
47+
Pagination paging.Page `json:"pagination"`
4848
}
4949

5050
// communityMemberSummary is a typed member entry for getCommunity.
@@ -60,7 +60,7 @@ type getCommunityResponse struct {
6060
DerivedState map[string]any `json:"derived_state"`
6161
Coverage *float64 `json:"coverage,omitempty"`
6262
Members []communityMemberSummary `json:"members,omitempty"`
63-
MembersPagination map[string]any `json:"members_pagination,omitempty"`
63+
MembersPagination *paging.Page `json:"members_pagination,omitempty"`
6464
}
6565

6666
// archCommCount is a helper struct for counting community nodes in architecture overview.
@@ -92,9 +92,9 @@ type architectureOverviewCoupling struct {
9292
// @intent preserve the legacy architecture overview response shape with typed fields.
9393
type architectureOverviewResponse struct {
9494
Communities []architectureOverviewCommunity `json:"communities"`
95-
CommunitiesPagination map[string]any `json:"communities_pagination"`
95+
CommunitiesPagination paging.Page `json:"communities_pagination"`
9696
Coupling []architectureOverviewCoupling `json:"coupling"`
97-
CouplingPagination map[string]any `json:"coupling_pagination"`
97+
CouplingPagination paging.Page `json:"coupling_pagination"`
9898
Warnings []string `json:"warnings"`
9999
DerivedState map[string]any `json:"derived_state"`
100100
}
@@ -182,7 +182,7 @@ func (h *handlers) listFlows(ctx context.Context, request mcp.CallToolRequest) (
182182
result, err := marshalJSON(listFlowsResponse{
183183
Flows: infos,
184184
DerivedState: derivedStateFlows(),
185-
Pagination: buildPaginationMetadata(limit, offset, len(infos), hasMore),
185+
Pagination: paging.BuildPage(paging.Request{Limit: limit, Offset: offset}, len(infos), hasMore),
186186
})
187187
if err != nil {
188188
return "", trace.Wrap(err, "marshal result")
@@ -255,7 +255,7 @@ func (h *handlers) listCommunities(ctx context.Context, request mcp.CallToolRequ
255255
result, err := marshalJSON(listCommunitiesResponse{
256256
Communities: infos,
257257
DerivedState: derivedStateCommunities(),
258-
Pagination: buildPaginationMetadata(limit, offset, len(infos), hasMore),
258+
Pagination: paging.BuildPage(paging.Request{Limit: limit, Offset: offset}, len(infos), hasMore),
259259
})
260260
if err != nil {
261261
return "", trace.Wrap(err, "marshal result")
@@ -350,7 +350,8 @@ func (h *handlers) getCommunity(ctx context.Context, request mcp.CallToolRequest
350350
members[i] = nodeToSummary(n)
351351
}
352352
gcData.Members = members
353-
gcData.MembersPagination = buildPaginationMetadata(memberLimit, memberOffset, len(members), hasMore)
353+
page := paging.BuildPage(paging.Request{Limit: memberLimit, Offset: memberOffset}, len(members), hasMore)
354+
gcData.MembersPagination = &page
354355
}
355356

356357
result, err := marshalJSON(gcData)
@@ -443,9 +444,9 @@ func (h *handlers) getArchitectureOverview(ctx context.Context, request mcp.Call
443444

444445
result, err := marshalJSON(architectureOverviewResponse{
445446
Communities: commInfos,
446-
CommunitiesPagination: buildPaginationMetadata(communityLimit, communityOffset, len(commInfos), communityHasMore),
447+
CommunitiesPagination: paging.BuildPage(paging.Request{Limit: communityLimit, Offset: communityOffset}, len(commInfos), communityHasMore),
447448
Coupling: couplingPairs,
448-
CouplingPagination: buildPaginationMetadata(couplingLimit, couplingOffset, len(couplingPairs), couplingHasMore),
449+
CouplingPagination: paging.BuildPage(paging.Request{Limit: couplingLimit, Offset: couplingOffset}, len(couplingPairs), couplingHasMore),
449450
Warnings: warnings,
450451
DerivedState: derivedStateSummary(),
451452
})

internal/mcp/handlers.go

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -138,22 +138,6 @@ func validateOffset(offset int) error {
138138
return nil
139139
}
140140

141-
// buildPaginationMetadata builds a compact pagination descriptor for tool consumers.
142-
// @intent provide one consistent pagination metadata shape across tool responses.
143-
// @ensures includes next_offset only when hasMore is true.
144-
func buildPaginationMetadata(limit, offset, returned int, hasMore bool) map[string]any {
145-
pagination := map[string]any{
146-
"limit": limit,
147-
"offset": offset,
148-
"returned": returned,
149-
"has_more": hasMore,
150-
}
151-
if hasMore {
152-
pagination["next_offset"] = offset + limit
153-
}
154-
return pagination
155-
}
156-
157141
// unwrapToolResultErr extracts an embedded MCP tool result from an error.
158142
// @intent recover user-facing MCP tool results from the internal error flow at one shared exit point.
159143
// @return returns the embedded MCP result and true when err is a toolResultErr.
@@ -250,17 +234,3 @@ func nodeToSummary(n model.Node) nodeSummary {
250234
FilePath: n.FilePath,
251235
}
252236
}
253-
254-
// nodeToBasicMap converts a graph node into a compact response payload.
255-
// @intent preserve the legacy map-based node shape for existing MCP callers.
256-
// @param n is the graph node to include in an MCP response.
257-
// @return returns a map with identifier, name, kind, and file path fields.
258-
func nodeToBasicMap(n model.Node) map[string]any {
259-
return map[string]any{
260-
"id": n.ID,
261-
"qualified_name": n.QualifiedName,
262-
"kind": n.Kind,
263-
"name": n.Name,
264-
"file_path": n.FilePath,
265-
}
266-
}

0 commit comments

Comments
 (0)