feat: add package indexing to MCP index_revision tool

The options server's index_revision now also indexes packages when running
under nixpkgs-search, matching the CLI behavior. The packages server gets
its own index_revision tool for standalone package indexing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 22:12:08 +01:00
parent a0be405b76
commit fd40e73f1b
7 changed files with 260 additions and 14 deletions

View File

@@ -14,12 +14,29 @@ import (
)
// RegisterHandlers registers all tool handlers on the server for options mode.
// Used by legacy nixos-options and hm-options servers (no package indexing).
func (s *Server) RegisterHandlers(indexer options.Indexer) {
s.registerOptionsHandlers(indexer, nil)
}
// RegisterHandlersWithPackages registers all tool handlers for options mode
// with additional package indexing support. When pkgIndexer is non-nil,
// index_revision will also index packages, and list_revisions will show package counts.
func (s *Server) RegisterHandlersWithPackages(indexer options.Indexer, pkgIndexer *packages.Indexer) {
s.registerOptionsHandlers(indexer, pkgIndexer)
}
// registerOptionsHandlers is the shared implementation for RegisterHandlers and RegisterHandlersWithPackages.
func (s *Server) registerOptionsHandlers(indexer options.Indexer, pkgIndexer *packages.Indexer) {
s.tools["search_options"] = s.handleSearchOptions
s.tools["get_option"] = s.handleGetOption
s.tools["get_file"] = s.handleGetFile
s.tools["index_revision"] = s.makeIndexHandler(indexer)
s.tools["list_revisions"] = s.handleListRevisions
s.tools["index_revision"] = s.makeIndexHandler(indexer, pkgIndexer)
if pkgIndexer != nil {
s.tools["list_revisions"] = s.handleListRevisionsWithPackages
} else {
s.tools["list_revisions"] = s.handleListRevisions
}
s.tools["delete_revision"] = s.handleDeleteRevision
}
@@ -28,6 +45,7 @@ func (s *Server) RegisterPackageHandlers(pkgIndexer *packages.Indexer) {
s.tools["search_packages"] = s.handleSearchPackages
s.tools["get_package"] = s.handleGetPackage
s.tools["get_file"] = s.handleGetFile
s.tools["index_revision"] = s.makePackageIndexHandler(pkgIndexer)
s.tools["list_revisions"] = s.handleListRevisionsWithPackages
s.tools["delete_revision"] = s.handleDeleteRevision
}
@@ -246,7 +264,8 @@ func (s *Server) handleGetFile(ctx context.Context, args map[string]interface{})
}
// makeIndexHandler creates the index_revision handler with the indexer.
func (s *Server) makeIndexHandler(indexer options.Indexer) ToolHandler {
// If pkgIndexer is non-nil, it will also index packages after options and files.
func (s *Server) makeIndexHandler(indexer options.Indexer, pkgIndexer *packages.Indexer) ToolHandler {
return func(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
revision, _ := args["revision"].(string)
if revision == "" {
@@ -278,6 +297,17 @@ func (s *Server) makeIndexHandler(indexer options.Indexer) ToolHandler {
s.logger.Printf("Warning: file indexing failed: %v", err)
}
// Index packages if package indexer is available
var packageCount int
if pkgIndexer != nil {
pkgResult, pkgErr := pkgIndexer.IndexPackages(ctx, result.Revision.ID, result.Revision.GitHash)
if pkgErr != nil {
s.logger.Printf("Warning: package indexing failed: %v", pkgErr)
} else {
packageCount = pkgResult.PackageCount
}
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Indexed revision: %s\n", result.Revision.GitHash))
if result.Revision.ChannelName != "" {
@@ -285,6 +315,9 @@ func (s *Server) makeIndexHandler(indexer options.Indexer) ToolHandler {
}
sb.WriteString(fmt.Sprintf("Options: %d\n", result.OptionCount))
sb.WriteString(fmt.Sprintf("Files: %d\n", fileCount))
if packageCount > 0 {
sb.WriteString(fmt.Sprintf("Packages: %d\n", packageCount))
}
// Handle Duration which may be time.Duration or interface{}
if dur, ok := result.Duration.(time.Duration); ok {
sb.WriteString(fmt.Sprintf("Duration: %s\n", dur.Round(time.Millisecond)))
@@ -296,6 +329,85 @@ func (s *Server) makeIndexHandler(indexer options.Indexer) ToolHandler {
}
}
// makePackageIndexHandler creates an index_revision handler for the packages-only server.
// It creates a revision record if needed, then indexes packages.
func (s *Server) makePackageIndexHandler(pkgIndexer *packages.Indexer) ToolHandler {
return func(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
revision, _ := args["revision"].(string)
if revision == "" {
return ErrorContent(fmt.Errorf("revision is required")), nil
}
if err := packages.ValidateRevision(revision); err != nil {
return ErrorContent(err), nil
}
// Resolve channel aliases to git ref
ref := pkgIndexer.ResolveRevision(revision)
// Check if revision already exists
rev, err := s.store.GetRevision(ctx, ref)
if err != nil {
return ErrorContent(fmt.Errorf("failed to check revision: %w", err)), nil
}
if rev == nil {
// Also try by channel name
rev, err = s.store.GetRevisionByChannel(ctx, revision)
if err != nil {
return ErrorContent(fmt.Errorf("failed to check revision: %w", err)), nil
}
}
if rev == nil {
// Create a new revision record
commitDate, _ := pkgIndexer.GetCommitDate(ctx, ref)
rev = &database.Revision{
GitHash: ref,
ChannelName: pkgIndexer.GetChannelName(revision),
CommitDate: commitDate,
}
if err := s.store.CreateRevision(ctx, rev); err != nil {
return ErrorContent(fmt.Errorf("failed to create revision: %w", err)), nil
}
}
// Check if packages are already indexed for this revision
if rev.PackageCount > 0 {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Revision already indexed: %s\n", rev.GitHash))
if rev.ChannelName != "" {
sb.WriteString(fmt.Sprintf("Channel: %s\n", rev.ChannelName))
}
sb.WriteString(fmt.Sprintf("Packages: %d\n", rev.PackageCount))
sb.WriteString(fmt.Sprintf("Indexed at: %s\n", rev.IndexedAt.Format("2006-01-02 15:04")))
return CallToolResult{
Content: []Content{TextContent(sb.String())},
}, nil
}
// Index packages
pkgResult, err := pkgIndexer.IndexPackages(ctx, rev.ID, rev.GitHash)
if err != nil {
return ErrorContent(fmt.Errorf("package indexing failed: %w", err)), nil
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Indexed revision: %s\n", rev.GitHash))
if rev.ChannelName != "" {
sb.WriteString(fmt.Sprintf("Channel: %s\n", rev.ChannelName))
}
sb.WriteString(fmt.Sprintf("Packages: %d\n", pkgResult.PackageCount))
if dur, ok := pkgResult.Duration.(time.Duration); ok {
sb.WriteString(fmt.Sprintf("Duration: %s\n", dur.Round(time.Millisecond)))
}
return CallToolResult{
Content: []Content{TextContent(sb.String())},
}, nil
}
}
// handleListRevisions handles the list_revisions tool.
func (s *Server) handleListRevisions(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
revisions, err := s.store.ListRevisions(ctx)