Skip to content

Commit 9914239

Browse files
authored
fix: Add fallback channels when NixOS API discovery fails (#52) (#54)
* fix: Add fallback channels when NixOS API discovery fails (#52) Resolves issue where users encountered "Invalid channel 'stable'" errors when the NixOS API was unreachable or slow, leaving channel discovery with empty results. Changes: - Add static FALLBACK_CHANNELS mapping for reliable offline operation - Increase API timeout from 5s to 10s for slow connections - Remove deprecated channels (20.09, 24.11 EOL June 2025) - Add using_fallback flag to ChannelCache - Display warnings in nixos_channels() when fallback is active - Update 7 tests to verify fallback behavior - Ensure all channel-based tools work with fallback channels The fallback channels provide a safety net while maintaining the dynamic discovery feature for up-to-date channel information. * fix(tests): Update NixHub tests to reflect actual API data Ruby version positions in NixHub API have shifted as new versions were released. Updated tests to use Ruby 3.0.x (positions 36-42) instead of Ruby 2.6.7 (position 52), which is beyond the standard limit=50 upper bound. Changes: - test_ai_response_for_missing_version: Use Ruby 3.0 instead of 2.6 - test_efficient_search_strategy: Updated to test Ruby 3.0 - test_finding_older_ruby_version: Use Ruby 3.0 (within limit=50) - test_version_not_in_nixhub: Remove assertion for 2.6, test 2.7/3.0 These changes ensure tests remain stable as new Ruby versions are released to NixHub, while still testing the intended behavior of finding older versions with appropriate limits.
1 parent 46b4d4d commit 9914239

File tree

6 files changed

+312
-69
lines changed

6 files changed

+312
-69
lines changed

.claude/settings.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash",
5+
"Task",
6+
"Glob",
7+
"Grep",
8+
"LS",
9+
"Read",
10+
"Edit",
11+
"MultiEdit",
12+
"Write",
13+
"WebFetch",
14+
"WebSearch"
15+
]
16+
},
17+
"enabledMcpjsonServers": [
18+
"nixos"
19+
]
20+
}

mcp_nixos/server.py

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ class DocumentParseError(Exception):
3838
"25.05": "nixos-25.05",
3939
}
4040

41+
# Fallback channels when API discovery fails
42+
# These are static mappings based on most recent known patterns
43+
FALLBACK_CHANNELS = {
44+
"unstable": "latest-44-nixos-unstable",
45+
"stable": "latest-44-nixos-25.05",
46+
"25.05": "latest-44-nixos-25.05",
47+
"25.11": "latest-44-nixos-25.11", # For when 25.11 is released
48+
"beta": "latest-44-nixos-25.05",
49+
}
50+
4151
HOME_MANAGER_URL = "https://nix-community.github.io/home-manager/options.xhtml"
4252
DARWIN_URL = "https://nix-darwin.github.io/nix-darwin/manual/index.html"
4353

@@ -49,6 +59,7 @@ def __init__(self) -> None:
4959
"""Initialize empty cache."""
5060
self.available_channels: dict[str, str] | None = None
5161
self.resolved_channels: dict[str, str] | None = None
62+
self.using_fallback: bool = False
5263

5364
def get_available(self) -> dict[str, str]:
5465
"""Get available channels, discovering if needed."""
@@ -66,7 +77,8 @@ def _discover_available_channels(self) -> dict[str, str]:
6677
"""Discover available NixOS channels by testing API patterns."""
6778
# Test multiple generation patterns (43, 44, 45) and versions
6879
generations = [43, 44, 45, 46] # Future-proof
69-
versions = ["unstable", "20.09", "24.11", "25.05", "25.11", "26.05", "30.05"] # Past, current and future
80+
# Removed deprecated versions (20.09, 24.11 - EOL June 2025)
81+
versions = ["unstable", "25.05", "25.11", "26.05", "30.05"] # Current and future
7082

7183
available = {}
7284
for gen in generations:
@@ -77,7 +89,7 @@ def _discover_available_channels(self) -> dict[str, str]:
7789
f"{NIXOS_API}/{pattern}/_count",
7890
json={"query": {"match_all": {}}},
7991
auth=NIXOS_AUTH,
80-
timeout=5,
92+
timeout=10, # Increased from 5s to 10s for slow connections
8193
)
8294
if resp.status_code == 200:
8395
count = resp.json().get("count", 0)
@@ -91,6 +103,12 @@ def _discover_available_channels(self) -> dict[str, str]:
91103
def _resolve_channels(self) -> dict[str, str]:
92104
"""Resolve user-friendly channel names to actual indices."""
93105
available = self.get_available()
106+
107+
# If no channels were discovered, use fallback channels
108+
if not available:
109+
self.using_fallback = True
110+
return FALLBACK_CHANNELS.copy()
111+
94112
resolved = {}
95113

96114
# Find unstable (should be consistent)
@@ -140,6 +158,11 @@ def _resolve_channels(self) -> dict[str, str]:
140158
if "stable" in resolved:
141159
resolved["beta"] = resolved["stable"]
142160

161+
# If we still have no channels after all that, use fallback
162+
if not resolved:
163+
self.using_fallback = True
164+
return FALLBACK_CHANNELS.copy()
165+
143166
return resolved
144167

145168

@@ -523,7 +546,15 @@ async def nixos_channels() -> str:
523546
available = channel_cache.get_available()
524547

525548
results = []
526-
results.append("NixOS Channels (auto-discovered):\n")
549+
550+
# Show warning if using fallback channels
551+
if channel_cache.using_fallback:
552+
results.append("⚠️ WARNING: Using fallback channels (API discovery failed)")
553+
results.append(" Check network connectivity to search.nixos.org")
554+
results.append("")
555+
results.append("NixOS Channels (fallback mode):\n")
556+
else:
557+
results.append("NixOS Channels (auto-discovered):\n")
527558

528559
# Show user-friendly channel names
529560
for name, index in sorted(configured.items()):
@@ -543,19 +574,26 @@ async def nixos_channels() -> str:
543574
if index in available:
544575
results.append(f" Status: {status} ({doc_count})")
545576
else:
546-
results.append(f" Status: {status}")
577+
if channel_cache.using_fallback:
578+
results.append(" Status: Fallback (may not be current)")
579+
else:
580+
results.append(f" Status: {status}")
547581
results.append("")
548582

549583
# Show additional discovered channels not in our mapping
550-
discovered_only = set(available.keys()) - set(configured.values())
551-
if discovered_only:
552-
results.append("Additional available channels:")
553-
for index in sorted(discovered_only):
554-
results.append(f"• {index} ({available[index]})")
584+
if not channel_cache.using_fallback:
585+
discovered_only = set(available.keys()) - set(configured.values())
586+
if discovered_only:
587+
results.append("Additional available channels:")
588+
for index in sorted(discovered_only):
589+
results.append(f"• {index} ({available[index]})")
555590

556591
# Add deprecation warnings
557592
results.append("\nNote: Channels are dynamically discovered.")
558593
results.append("'stable' always points to the current stable release.")
594+
if channel_cache.using_fallback:
595+
results.append("\n⚠️ Fallback channels may not reflect the latest available versions.")
596+
results.append(" Please check your network connection to search.nixos.org.")
559597

560598
return "\n".join(results).strip()
561599
except Exception as e:

0 commit comments

Comments
 (0)