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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user