This repository has been archived on 2026-03-10. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
labmcp/internal/nixos/parser.go
Torjus Håkestad 0b0ada3ccd feat: MCP tools and nixpkgs indexer
- Add options.json parser with mdDoc support
- Add nixpkgs indexer using nix-build
- Implement all MCP tool handlers:
  - search_options: Full-text search with filters
  - get_option: Option details with children
  - get_file: Fetch file contents
  - index_revision: Build and index options
  - list_revisions: Show indexed versions
  - delete_revision: Remove indexed data
- Add parser tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 17:54:42 +01:00

127 lines
3.5 KiB
Go

package nixos
import (
"encoding/json"
"fmt"
"io"
)
// ParseOptions parses the options.json file from nixpkgs.
// The options.json structure is a map from option name to option definition.
func ParseOptions(r io.Reader) (map[string]*ParsedOption, error) {
var raw map[string]json.RawMessage
if err := json.NewDecoder(r).Decode(&raw); err != nil {
return nil, fmt.Errorf("failed to decode options.json: %w", err)
}
options := make(map[string]*ParsedOption, len(raw))
for name, data := range raw {
opt, err := parseOption(name, data)
if err != nil {
// Log but don't fail - some options might have unusual formats
continue
}
options[name] = opt
}
return options, nil
}
// ParsedOption represents a parsed NixOS option with all its metadata.
type ParsedOption struct {
Name string
Type string
Description string
Default string // JSON-encoded value
Example string // JSON-encoded value
ReadOnly bool
Declarations []string
}
// optionJSON is the internal structure for parsing options.json entries.
type optionJSON struct {
Declarations []string `json:"declarations"`
Default json.RawMessage `json:"default,omitempty"`
Description interface{} `json:"description"` // Can be string or object
Example json.RawMessage `json:"example,omitempty"`
ReadOnly bool `json:"readOnly"`
Type string `json:"type"`
}
// parseOption parses a single option entry.
func parseOption(name string, data json.RawMessage) (*ParsedOption, error) {
var opt optionJSON
if err := json.Unmarshal(data, &opt); err != nil {
return nil, fmt.Errorf("failed to parse option %s: %w", name, err)
}
// Handle description which can be a string or an object with _type: "mdDoc"
description := extractDescription(opt.Description)
// Convert declarations to relative paths
declarations := make([]string, 0, len(opt.Declarations))
for _, d := range opt.Declarations {
declarations = append(declarations, normalizeDeclarationPath(d))
}
return &ParsedOption{
Name: name,
Type: opt.Type,
Description: description,
Default: string(opt.Default),
Example: string(opt.Example),
ReadOnly: opt.ReadOnly,
Declarations: declarations,
}, nil
}
// extractDescription extracts the description string from various formats.
func extractDescription(desc interface{}) string {
switch v := desc.(type) {
case string:
return v
case map[string]interface{}:
// Handle mdDoc format: {"_type": "mdDoc", "text": "..."}
if text, ok := v["text"].(string); ok {
return text
}
// Try "description" key
if text, ok := v["description"].(string); ok {
return text
}
}
return ""
}
// normalizeDeclarationPath converts a full store path to a relative nixpkgs path.
// Input: "/nix/store/xxx-source/nixos/modules/services/web-servers/nginx/default.nix"
// Output: "nixos/modules/services/web-servers/nginx/default.nix"
func normalizeDeclarationPath(path string) string {
// Look for common prefixes and strip them
markers := []string{
"/nixos/",
"/pkgs/",
"/lib/",
"/maintainers/",
}
for _, marker := range markers {
if idx := findSubstring(path, marker); idx >= 0 {
return path[idx+1:] // +1 to skip the leading /
}
}
// If no marker found, return as-is
return path
}
// findSubstring returns the index of the first occurrence of substr in s, or -1.
func findSubstring(s, substr string) int {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return i
}
}
return -1
}