Files
mcp/internal/tools/search_symbols.go
Rene Nochebuena 13a186c60a feat(mcp): index struct fields and interface method sets (#1)
Minor release. The indexer named composite types but could not describe
their shape: every declaration was truncated at the first brace, so a
struct symbol carried only its `type X struct` header and an interface
symbol only its `type X interface` header. Field names, field types, and
— most painfully — struct tags such as `env:"EINHERJAR_PG_HOST"` were
dropped, as were the method sets of every port interface. An assistant
could be told that `db-postgres` has a `Config` and a `Provider`, but not
what env vars configure the one or what methods the other requires. This
change captures both.

internal/index (schema):
- Symbol gains two optional fields. `fields` ([]Field) carries a struct's
  field set — name, type, raw struct tag (surrounding backticks stripped),
  doc comment, and an `embedded` marker. `methods` ([]Method) carries an
  interface's method set — name, signature without the leading `func`, and
  doc comment, including embedded interfaces. Both are omitempty and absent
  for every other kind.
- SchemaVersion is deliberately unchanged. The two additions are additive
  and omitempty, so an older consumer parses the new index unchanged; per
  the existing rule the constant only bumps on a breaking format change.

internal/index (builder):
- collectSymbols now inspects each type's TypeSpec and, for a *ast.StructType
  or *ast.InterfaceType, fills the new Symbol members. A grouped field
  declaration (`x, y int`) yields one Field per name; an embedded field or
  interface yields an entry with an empty name.
- New helpers: typeSpecType (underlying type expr of a lone type spec),
  extractFields, extractIfaceMethods, fieldDoc (doc comment or trailing
  line comment), and nodeString — a non-truncating printer used for field
  types, tags, and method signatures, distinct from formatNode which keeps
  truncating to produce the one-line header.

internal/index (search):
- matches() now also tests the query against struct field names, field
  types, and struct tags, and against interface method names and
  signatures. A query like an env-var key or a method name now resolves to
  the type that declares it.

internal/tools:
- get_symbol and search_symbols descriptions updated to advertise the new
  struct-field and interface-method coverage. No input/output schema change
  beyond the additive Symbol fields, which get_symbol already returns whole.

internal/index (tests):
- New builder_test.go — the package previously had no tests. Builds a
  temporary module fixture and asserts capture of struct fields (with tags
  and docs), embedded fields, interface methods (with signatures and docs),
  embedded interfaces, and discovery of a struct by one of its struct tags
  (the failure mode that motivated the change).

Docs:
- CHANGELOG.md gains an [Unreleased] entry; README.md tool table updated to
  state that get_symbol returns struct fields and interface methods and
  that search_symbols matches fields, tags, and methods.

No new dependencies. The committed data/index.json placeholder is untouched
— the index is regenerated at image build (Dockerfile runs cmd/indexer),
so a deployment must be rebuilt to serve the richer index; a server still
running the prior image keeps serving the older, member-less one.

Reviewed-on: #1
Co-authored-by: Rene Nochebuena Guerrero <rene@nochebuena.dev>
Co-committed-by: Rene Nochebuena Guerrero <rene@nochebuena.dev>
2026-06-10 10:38:30 -06:00

49 lines
1.8 KiB
Go

package tools
import (
"context"
"code.nochebuena.dev/einherjar/mcp/internal/index"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type searchSymbolsInput struct {
Query string `json:"query" jsonschema:"text to match against symbol name, doc comment, sub-package, or module"`
Limit int `json:"limit,omitempty" jsonschema:"max results (default 25)"`
Module string `json:"module,omitempty" jsonschema:"restrict to one module by name"`
Kind string `json:"kind,omitempty" jsonschema:"restrict to one kind: type, interface, func, method, const, var"`
}
type searchSymbolsOutput struct {
Results []index.Symbol `json:"results"`
Total int `json:"total"`
}
func registerSearchSymbols(s *mcp.Server, idx *index.Index) {
mcp.AddTool(s, &mcp.Tool{
Name: "search_symbols",
Description: "Search Einherjar's exported symbols (types, interfaces, funcs, methods, consts, vars) by name or doc text. Structs also match on a field name, field type, or struct tag (e.g. an env-var key); interfaces also match on a required method. Optionally filter by module or kind. Use when you need to find where a type, function, field, or tag lives.",
}, func(ctx context.Context, req *mcp.CallToolRequest, args searchSymbolsInput) (*mcp.CallToolResult, searchSymbolsOutput, error) {
all := idx.SearchSymbols(args.Query, args.Limit*4+25)
filtered := all[:0]
for _, sym := range all {
if args.Module != "" && sym.Module != args.Module {
continue
}
if args.Kind != "" && sym.Kind != args.Kind {
continue
}
filtered = append(filtered, sym)
}
limit := args.Limit
if limit <= 0 {
limit = 25
}
if len(filtered) > limit {
filtered = filtered[:limit]
}
out := searchSymbolsOutput{Results: filtered, Total: len(filtered)}
return jsonText(out), out, nil
})
}