Skip to content

Commit 3356343

Browse files
committed
test(github): address reviewer feedback on ReadOnlyHint check
- Resolve each file's local alias for github.com/modelcontextprotocol/go-sdk/mcp via file.Imports rather than hard-coding the "mcp" qualifier, so the check also covers files that import the SDK under a non-default alias. - Detect positional (unkeyed) composite literals and report a dedicated diagnostic instead of producing misleading "missing field" violations. - Drop the brittle 'expected to discover at least one mcp.Tool literal' assertion: if registrations move behind constructors/factories the AST walker legitimately finds nothing. - Use strconv.Unquote to decode tool-name string literals (handles escapes in interpreted strings); fall back to the raw lexeme on parse error.
1 parent a973416 commit 3356343

1 file changed

Lines changed: 86 additions & 28 deletions

File tree

pkg/github/tools_static_validation_test.go

Lines changed: 86 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import (
1313
"github.com/stretchr/testify/require"
1414
)
1515

16+
// mcpImportPath is the canonical module path of the MCP go-sdk that pkg/github
17+
// imports as `mcp` (or under an alias). Per-file alias resolution lets this
18+
// test correctly identify mcp.Tool / mcp.ToolAnnotations literals even when a
19+
// file imports the SDK under a non-default local name.
20+
const mcpImportPath = "github.com/modelcontextprotocol/go-sdk/mcp"
21+
1622
// TestAllToolRegistrationsExplicitlySetReadOnlyHint statically scans every
1723
// non-test Go source file in this package and asserts that every mcp.Tool
1824
// composite literal explicitly sets Annotations.ReadOnlyHint.
@@ -45,19 +51,22 @@ func TestAllToolRegistrationsExplicitlySetReadOnlyHint(t *testing.T) {
4551
reason string
4652
}
4753
var violations []violation
48-
literalsSeen := 0
4954

5055
for _, pkg := range pkgs {
5156
for filename, file := range pkg.Files {
57+
aliases := mcpAliasesFor(file)
58+
if len(aliases) == 0 {
59+
// File does not import the MCP go-sdk; no tool literals possible.
60+
continue
61+
}
5262
ast.Inspect(file, func(n ast.Node) bool {
5363
cl, ok := n.(*ast.CompositeLit)
5464
if !ok {
5565
return true
5666
}
57-
if !isMCPToolType(cl.Type) {
67+
if !isQualifiedType(cl.Type, aliases, "Tool") {
5868
return true
5969
}
60-
literalsSeen++
6170

6271
toolName := extractToolName(cl)
6372
if toolName == "" {
@@ -69,6 +78,16 @@ func TestAllToolRegistrationsExplicitlySetReadOnlyHint(t *testing.T) {
6978
rel = filepath.Base(filename)
7079
}
7180

81+
if hasUnkeyedFields(cl) {
82+
violations = append(violations, violation{
83+
file: rel,
84+
line: pos.Line,
85+
toolName: toolName,
86+
reason: "mcp.Tool literal uses positional (unkeyed) fields; this check requires keyed fields so Annotations.ReadOnlyHint can be verified",
87+
})
88+
return true
89+
}
90+
7291
annotations := findFieldValue(cl, "Annotations")
7392
if annotations == nil {
7493
violations = append(violations, violation{
@@ -80,7 +99,7 @@ func TestAllToolRegistrationsExplicitlySetReadOnlyHint(t *testing.T) {
8099
return true
81100
}
82101

83-
annoLit := unwrapAnnotationsLiteral(annotations)
102+
annoLit := unwrapAnnotationsLiteral(annotations, aliases)
84103
if annoLit == nil {
85104
// Annotations is set to something we can't statically
86105
// verify (e.g. a function call). Flag it so reviewers
@@ -94,6 +113,16 @@ func TestAllToolRegistrationsExplicitlySetReadOnlyHint(t *testing.T) {
94113
return true
95114
}
96115

116+
if hasUnkeyedFields(annoLit) {
117+
violations = append(violations, violation{
118+
file: rel,
119+
line: pos.Line,
120+
toolName: toolName,
121+
reason: "mcp.ToolAnnotations literal uses positional (unkeyed) fields; use keyed fields so ReadOnlyHint can be verified",
122+
})
123+
return true
124+
}
125+
97126
if findFieldValue(annoLit, "ReadOnlyHint") == nil {
98127
violations = append(violations, violation{
99128
file: rel,
@@ -107,8 +136,9 @@ func TestAllToolRegistrationsExplicitlySetReadOnlyHint(t *testing.T) {
107136
}
108137
}
109138

110-
require.NotZero(t, literalsSeen,
111-
"expected to discover at least one mcp.Tool literal; AST walker may be broken")
139+
// Intentionally do not assert that any literals were observed: if tool
140+
// registrations move behind constructors/factories there may be nothing
141+
// for this check to validate, and that is a legitimate state.
112142

113143
if len(violations) > 0 {
114144
var msg strings.Builder
@@ -131,21 +161,32 @@ func TestAllToolRegistrationsExplicitlySetReadOnlyHint(t *testing.T) {
131161
}
132162
}
133163

134-
// isMCPToolType reports whether the given AST expression refers to mcp.Tool.
135-
func isMCPToolType(expr ast.Expr) bool {
136-
sel, ok := expr.(*ast.SelectorExpr)
137-
if !ok {
138-
return false
139-
}
140-
ident, ok := sel.X.(*ast.Ident)
141-
if !ok {
142-
return false
164+
// mcpAliasesFor returns the set of local identifiers under which the given
165+
// file imports the MCP go-sdk (mcpImportPath). The default unaliased import
166+
// resolves to the package name "mcp". Blank (`_`) and dot (`.`) imports are
167+
// skipped because tool literals cannot meaningfully be qualified through them.
168+
func mcpAliasesFor(file *ast.File) map[string]struct{} {
169+
aliases := map[string]struct{}{}
170+
for _, imp := range file.Imports {
171+
path, err := strconv.Unquote(imp.Path.Value)
172+
if err != nil || path != mcpImportPath {
173+
continue
174+
}
175+
if imp.Name != nil {
176+
if imp.Name.Name == "_" || imp.Name.Name == "." {
177+
continue
178+
}
179+
aliases[imp.Name.Name] = struct{}{}
180+
continue
181+
}
182+
aliases["mcp"] = struct{}{}
143183
}
144-
return ident.Name == "mcp" && sel.Sel != nil && sel.Sel.Name == "Tool"
184+
return aliases
145185
}
146186

147-
// isMCPToolAnnotationsType reports whether the given AST expression refers to mcp.ToolAnnotations.
148-
func isMCPToolAnnotationsType(expr ast.Expr) bool {
187+
// isQualifiedType reports whether expr is a SelectorExpr of the form
188+
// <alias>.<typeName> where alias is in the provided alias set.
189+
func isQualifiedType(expr ast.Expr, aliases map[string]struct{}, typeName string) bool {
149190
sel, ok := expr.(*ast.SelectorExpr)
150191
if !ok {
151192
return false
@@ -154,7 +195,23 @@ func isMCPToolAnnotationsType(expr ast.Expr) bool {
154195
if !ok {
155196
return false
156197
}
157-
return ident.Name == "mcp" && sel.Sel != nil && sel.Sel.Name == "ToolAnnotations"
198+
if _, ok := aliases[ident.Name]; !ok {
199+
return false
200+
}
201+
return sel.Sel != nil && sel.Sel.Name == typeName
202+
}
203+
204+
// hasUnkeyedFields reports whether the composite literal has any positional
205+
// (non-key/value) elements. The static check cannot reliably map positional
206+
// fields without full type information, so such literals are rejected with a
207+
// dedicated diagnostic rather than producing false "missing field" violations.
208+
func hasUnkeyedFields(cl *ast.CompositeLit) bool {
209+
for _, elt := range cl.Elts {
210+
if _, ok := elt.(*ast.KeyValueExpr); !ok {
211+
return true
212+
}
213+
}
214+
return false
158215
}
159216

160217
// findFieldValue returns the value expression for the named keyed field of a
@@ -177,24 +234,27 @@ func findFieldValue(cl *ast.CompositeLit, name string) ast.Expr {
177234
}
178235

179236
// unwrapAnnotationsLiteral attempts to extract the *ast.CompositeLit for
180-
// &mcp.ToolAnnotations{...} or mcp.ToolAnnotations{...} from an expression.
181-
// Returns nil if the expression is not a statically inspectable literal.
182-
func unwrapAnnotationsLiteral(expr ast.Expr) *ast.CompositeLit {
237+
// &mcp.ToolAnnotations{...} or mcp.ToolAnnotations{...} from an expression,
238+
// resolving the MCP package's local alias per file.
239+
func unwrapAnnotationsLiteral(expr ast.Expr, aliases map[string]struct{}) *ast.CompositeLit {
183240
if u, ok := expr.(*ast.UnaryExpr); ok && u.Op == token.AND {
184241
expr = u.X
185242
}
186243
cl, ok := expr.(*ast.CompositeLit)
187244
if !ok {
188245
return nil
189246
}
190-
if !isMCPToolAnnotationsType(cl.Type) {
247+
if !isQualifiedType(cl.Type, aliases, "ToolAnnotations") {
191248
return nil
192249
}
193250
return cl
194251
}
195252

196253
// extractToolName returns the literal value of the Name field of an mcp.Tool
197254
// composite literal, or empty string if the value is not a basic string literal.
255+
// Interpreted ("...") and raw (`...`) string literals are handled via
256+
// strconv.Unquote so embedded escapes are decoded correctly; the raw
257+
// literal value is returned as a best-effort fallback if unquoting fails.
198258
func extractToolName(cl *ast.CompositeLit) string {
199259
v := findFieldValue(cl, "Name")
200260
if v == nil {
@@ -204,10 +264,8 @@ func extractToolName(cl *ast.CompositeLit) string {
204264
if !ok || bl.Kind != token.STRING {
205265
return ""
206266
}
207-
// Strip surrounding quotes; tolerate raw strings too.
208-
s := bl.Value
209-
if len(s) >= 2 && (s[0] == '"' || s[0] == '`') {
210-
s = s[1 : len(s)-1]
267+
if unq, err := strconv.Unquote(bl.Value); err == nil {
268+
return unq
211269
}
212-
return s
270+
return bl.Value
213271
}

0 commit comments

Comments
 (0)