Skip to content

Commit ff590d7

Browse files
committed
refactor(mcp): builder helpers for tool defs to collapse Sonar dup gate
The previous attempt (map-dispatch in 9c1e511) fixed the 7-arm switch but Sonar's gate stayed at 3.49% because the actual duplicated 14 lines were the structurally identical InputSchema/Properties scaffolding repeated across the seven Tool struct literals — not the dispatcher. Introduce three small builder helpers — mkTool(name, desc, opts...), param(name, type, desc), and required(fields...) — that own the InputSchema initialisation and Property construction once. The toolDefs list collapses from 7 repeating struct-literal blocks (8-12 lines each) to 7 mkTool calls (3-5 lines each). Same surface, same JSON shape on the wire, no behaviour change. The helper types are unexported and only used here. LOC delta: -20 net (65 inserted, 85 deleted). Verified by go test ./internal/mcp/... -count=1 -race (full suite passes) and gofmt clean.
1 parent 9c1e511 commit ff590d7

1 file changed

Lines changed: 65 additions & 85 deletions

File tree

internal/mcp/tools.go

Lines changed: 65 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -21,92 +21,72 @@ const (
2121
// OtelContext MCP server. The surface was reduced from 21 to 7 in
2222
// 2026-05-24 so the platform survives 120 services on SQLite — see
2323
// docs/superpowers/specs/2026-05-24-mcp-7tool-sqlite-survival-design.md.
24+
// schemaOpt mutates an InputSchema being built by mkTool. Use param(...) and
25+
// required(...) to compose schemas without re-typing the InputSchema /
26+
// Properties scaffolding on every tool definition.
27+
type schemaOpt func(*InputSchema)
28+
29+
// param adds a single Property to the schema. Type is "string" or "number".
30+
func param(name, typ, desc string) schemaOpt {
31+
return func(s *InputSchema) {
32+
s.Properties[name] = Property{Type: typ, Description: desc}
33+
}
34+
}
35+
36+
// required marks one or more parameter names as required by JSON-schema.
37+
func required(fields ...string) schemaOpt {
38+
return func(s *InputSchema) { s.Required = append(s.Required, fields...) }
39+
}
40+
41+
// mkTool builds a Tool with a freshly-initialised InputSchema. Centralising
42+
// the InputSchema/Properties scaffolding here keeps the toolDefs list one
43+
// call per tool and avoids the repeated struct-literal boilerplate that
44+
// SonarCloud (rightly) flagged as duplication.
45+
func mkTool(name, desc string, opts ...schemaOpt) Tool {
46+
s := InputSchema{Type: "object", Properties: map[string]Property{}}
47+
for _, opt := range opts {
48+
opt(&s)
49+
}
50+
return Tool{Name: name, Description: desc, InputSchema: s}
51+
}
52+
2453
var toolDefs = []Tool{
25-
{
26-
Name: "get_anomaly_timeline",
27-
Description: "Returns recent anomalies with temporal causal links, optionally filtered by service. The triage entry point — answers \"what's wrong right now\".",
28-
InputSchema: InputSchema{
29-
Type: "object",
30-
Properties: map[string]Property{
31-
"since": {Type: "string", Description: "Start time RFC3339. Defaults to 1h ago."},
32-
"service": {Type: "string", Description: "Filter by service."},
33-
},
34-
},
35-
},
36-
{
37-
Name: "get_service_map",
38-
Description: "Returns the service topology with health scores, error rates, call counts, and dependency edges. Powered by the live GraphRAG.",
39-
InputSchema: InputSchema{
40-
Type: "object",
41-
Properties: map[string]Property{
42-
"depth": {Type: "number", Description: "Max traversal depth (default 3)."},
43-
"service": {Type: "string", Description: "Focus on a specific service and its neighbors."},
44-
},
45-
},
46-
},
47-
{
48-
Name: "get_service_health",
49-
Description: "Returns detailed health metrics for a specific service: error rate, latency percentiles, request rate, and active alerts.",
50-
InputSchema: InputSchema{
51-
Type: "object",
52-
Required: []string{"service_name"},
53-
Properties: map[string]Property{
54-
"service_name": {Type: "string", Description: "The service name to query."},
55-
},
56-
},
57-
},
58-
{
59-
Name: "root_cause_analysis",
60-
Description: "Ranked probable root causes with evidence: error chains, anomalous metrics, correlated logs.",
61-
InputSchema: InputSchema{
62-
Type: "object",
63-
Required: []string{"service"},
64-
Properties: map[string]Property{
65-
"service": {Type: "string", Description: "Service experiencing issues."},
66-
"time_range": {Type: "string", Description: "Lookback window. Defaults to '15m'."},
67-
},
68-
},
69-
},
70-
{
71-
Name: "impact_analysis",
72-
Description: "BFS downstream from a service to find all affected services and impact scores.",
73-
InputSchema: InputSchema{
74-
Type: "object",
75-
Required: []string{"service"},
76-
Properties: map[string]Property{
77-
"service": {Type: "string", Description: "Service to analyze blast radius for."},
78-
"depth": {Type: "number", Description: "Max traversal depth (default 5)."},
79-
},
80-
},
81-
},
82-
{
83-
Name: "trace_graph",
84-
Description: "Returns the full span tree for a trace with service names, durations, errors, and linked logs.",
85-
InputSchema: InputSchema{
86-
Type: "object",
87-
Required: []string{"trace_id"},
88-
Properties: map[string]Property{
89-
"trace_id": {Type: "string", Description: "The trace ID to visualize."},
90-
},
91-
},
92-
},
93-
{
94-
Name: "search_logs",
95-
Description: "Searches log entries by severity, service, body text, trace ID, and time range. Returns id, timestamp, severity, service_name, body, trace_id. **Limited to the last 24 hours** — windows entirely outside the 24h cap are rejected. Strongly recommend setting `service` and/or `severity` to scope the search; unscoped keyword queries scan large row counts when FTS5 is disabled. Use severity=ERROR to find errors, query= for full-text search, trace_id= to correlate with a trace. Use page= for pagination.",
96-
InputSchema: InputSchema{
97-
Type: "object",
98-
Properties: map[string]Property{
99-
"query": {Type: "string", Description: "Full-text search in log body."},
100-
"severity": {Type: "string", Description: "Filter by severity level: ERROR, WARN, INFO, DEBUG."},
101-
"service": {Type: "string", Description: "Filter by service name (exact match)."},
102-
"trace_id": {Type: "string", Description: "Filter logs belonging to a specific trace ID."},
103-
"start": {Type: "string", Description: "Start time RFC3339. Defaults to 24h ago. Cannot be earlier than now-24h; older values are clamped."},
104-
"end": {Type: "string", Description: "End time RFC3339. Defaults to now. Cannot exceed now; future values are clamped."},
105-
"limit": {Type: "number", Description: "Max results per page (default 50, max 200)."},
106-
"page": {Type: "number", Description: "Page number for pagination (default 0)."},
107-
},
108-
},
109-
},
54+
mkTool("get_anomaly_timeline", "Returns recent anomalies with temporal causal links, optionally filtered by service. The triage entry point — answers \"what's wrong right now\".",
55+
param("since", "string", "Start time RFC3339. Defaults to 1h ago."),
56+
param("service", "string", "Filter by service."),
57+
),
58+
mkTool("get_service_map", "Returns the service topology with health scores, error rates, call counts, and dependency edges. Powered by the live GraphRAG.",
59+
param("depth", "number", "Max traversal depth (default 3)."),
60+
param("service", "string", "Focus on a specific service and its neighbors."),
61+
),
62+
mkTool("get_service_health", "Returns detailed health metrics for a specific service: error rate, latency percentiles, request rate, and active alerts.",
63+
required("service_name"),
64+
param("service_name", "string", "The service name to query."),
65+
),
66+
mkTool("root_cause_analysis", "Ranked probable root causes with evidence: error chains, anomalous metrics, correlated logs.",
67+
required("service"),
68+
param("service", "string", "Service experiencing issues."),
69+
param("time_range", "string", "Lookback window. Defaults to '15m'."),
70+
),
71+
mkTool("impact_analysis", "BFS downstream from a service to find all affected services and impact scores.",
72+
required("service"),
73+
param("service", "string", "Service to analyze blast radius for."),
74+
param("depth", "number", "Max traversal depth (default 5)."),
75+
),
76+
mkTool("trace_graph", "Returns the full span tree for a trace with service names, durations, errors, and linked logs.",
77+
required("trace_id"),
78+
param("trace_id", "string", "The trace ID to visualize."),
79+
),
80+
mkTool("search_logs", "Searches log entries by severity, service, body text, trace ID, and time range. Returns id, timestamp, severity, service_name, body, trace_id. **Limited to the last 24 hours** — windows entirely outside the 24h cap are rejected. Strongly recommend setting `service` and/or `severity` to scope the search; unscoped keyword queries scan large row counts when FTS5 is disabled. Use severity=ERROR to find errors, query= for full-text search, trace_id= to correlate with a trace. Use page= for pagination.",
81+
param("query", "string", "Full-text search in log body."),
82+
param("severity", "string", "Filter by severity level: ERROR, WARN, INFO, DEBUG."),
83+
param("service", "string", "Filter by service name (exact match)."),
84+
param("trace_id", "string", "Filter logs belonging to a specific trace ID."),
85+
param("start", "string", "Start time RFC3339. Defaults to 24h ago. Cannot be earlier than now-24h; older values are clamped."),
86+
param("end", "string", "End time RFC3339. Defaults to now. Cannot exceed now; future values are clamped."),
87+
param("limit", "number", "Max results per page (default 50, max 200)."),
88+
param("page", "number", "Page number for pagination (default 0)."),
89+
),
11090
}
11191

11292
// mcpCtx returns a tenant-scoped context for repository calls. If the caller's

0 commit comments

Comments
 (0)