arch-ops-server 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arch_ops_server/__init__.py +79 -0
- arch_ops_server/aur.py +1132 -0
- arch_ops_server/pacman.py +319 -0
- arch_ops_server/py.typed +0 -0
- arch_ops_server/server.py +672 -0
- arch_ops_server/utils.py +272 -0
- arch_ops_server/wiki.py +244 -0
- arch_ops_server-0.1.0.dist-info/METADATA +109 -0
- arch_ops_server-0.1.0.dist-info/RECORD +11 -0
- arch_ops_server-0.1.0.dist-info/WHEEL +4 -0
- arch_ops_server-0.1.0.dist-info/entry_points.txt +3 -0
arch_ops_server/utils.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for Arch Linux MCP Server.
|
|
3
|
+
Provides platform detection, subprocess execution, and error handling.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, Dict, Any
|
|
12
|
+
|
|
13
|
+
# Configure logging to stderr (STDIO server requirement)
|
|
14
|
+
logging.basicConfig(
|
|
15
|
+
level=logging.INFO,
|
|
16
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
17
|
+
handlers=[logging.StreamHandler()]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_arch_linux() -> bool:
|
|
24
|
+
"""
|
|
25
|
+
Detect if the current system is Arch Linux.
|
|
26
|
+
|
|
27
|
+
Checks for:
|
|
28
|
+
1. /etc/arch-release file existence
|
|
29
|
+
2. Platform identification
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
bool: True if running on Arch Linux, False otherwise
|
|
33
|
+
"""
|
|
34
|
+
# Check for Arch release file
|
|
35
|
+
if Path("/etc/arch-release").exists():
|
|
36
|
+
logger.info("Detected Arch Linux via /etc/arch-release")
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
# Fallback check via platform info
|
|
40
|
+
try:
|
|
41
|
+
with open("/etc/os-release", "r") as f:
|
|
42
|
+
content = f.read()
|
|
43
|
+
if "Arch Linux" in content or "ID=arch" in content:
|
|
44
|
+
logger.info("Detected Arch Linux via /etc/os-release")
|
|
45
|
+
return True
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
logger.info("Not running on Arch Linux")
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Cache the result since it won't change during runtime
|
|
54
|
+
IS_ARCH = is_arch_linux()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def run_command(
|
|
58
|
+
cmd: list[str],
|
|
59
|
+
timeout: int = 10,
|
|
60
|
+
check: bool = True
|
|
61
|
+
) -> tuple[int, str, str]:
|
|
62
|
+
"""
|
|
63
|
+
Execute a command asynchronously with timeout protection.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
cmd: Command and arguments as list
|
|
67
|
+
timeout: Timeout in seconds (default: 10)
|
|
68
|
+
check: If True, raise exception on non-zero exit code
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Tuple of (exit_code, stdout, stderr)
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
asyncio.TimeoutError: If command exceeds timeout
|
|
75
|
+
RuntimeError: If check=True and command fails
|
|
76
|
+
"""
|
|
77
|
+
logger.debug(f"Executing command: {' '.join(cmd)}")
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
process = await asyncio.create_subprocess_exec(
|
|
81
|
+
*cmd,
|
|
82
|
+
stdout=asyncio.subprocess.PIPE,
|
|
83
|
+
stderr=asyncio.subprocess.PIPE
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
stdout, stderr = await asyncio.wait_for(
|
|
87
|
+
process.communicate(),
|
|
88
|
+
timeout=timeout
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
exit_code = process.returncode
|
|
92
|
+
stdout_str = stdout.decode('utf-8', errors='replace')
|
|
93
|
+
stderr_str = stderr.decode('utf-8', errors='replace')
|
|
94
|
+
|
|
95
|
+
logger.debug(f"Command exit code: {exit_code}")
|
|
96
|
+
|
|
97
|
+
if check and exit_code != 0:
|
|
98
|
+
raise RuntimeError(
|
|
99
|
+
f"Command failed with exit code {exit_code}: {stderr_str}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return exit_code, stdout_str, stderr_str
|
|
103
|
+
|
|
104
|
+
except asyncio.TimeoutError:
|
|
105
|
+
logger.error(f"Command timed out after {timeout}s: {' '.join(cmd)}")
|
|
106
|
+
raise
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Command execution failed: {e}")
|
|
109
|
+
raise
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def add_aur_warning(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
113
|
+
"""
|
|
114
|
+
Wrap AUR data with prominent safety warning.
|
|
115
|
+
|
|
116
|
+
The AUR contains user-produced content that may be outdated,
|
|
117
|
+
broken, or malicious. Always inspect PKGBUILDs before installation.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
data: Original AUR response data
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Dict with added warning metadata
|
|
124
|
+
"""
|
|
125
|
+
return {
|
|
126
|
+
"warning": (
|
|
127
|
+
"⚠️ AUR PACKAGE WARNING ⚠️\n"
|
|
128
|
+
"AUR packages are USER-PRODUCED content and are not officially supported.\n"
|
|
129
|
+
"These packages may be outdated, broken, or even malicious.\n"
|
|
130
|
+
"ALWAYS review the PKGBUILD and other files before installing.\n"
|
|
131
|
+
"Use at your own risk."
|
|
132
|
+
),
|
|
133
|
+
"data": data
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def create_error_response(
|
|
138
|
+
error_type: str,
|
|
139
|
+
message: str,
|
|
140
|
+
details: Optional[str] = None,
|
|
141
|
+
suggest_wiki_search: bool = True
|
|
142
|
+
) -> Dict[str, Any]:
|
|
143
|
+
"""
|
|
144
|
+
Create a structured error response with Wiki suggestions.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
error_type: Type of error (e.g., "NetworkError", "NotFound")
|
|
148
|
+
message: Human-readable error message
|
|
149
|
+
details: Optional additional details
|
|
150
|
+
suggest_wiki_search: Whether to suggest related Wiki searches (default: True)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Structured error dict with Wiki suggestions
|
|
154
|
+
"""
|
|
155
|
+
response = {
|
|
156
|
+
"error": True,
|
|
157
|
+
"type": error_type,
|
|
158
|
+
"message": message
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if details:
|
|
162
|
+
response["details"] = details
|
|
163
|
+
|
|
164
|
+
# Add Wiki suggestions for common error types
|
|
165
|
+
if suggest_wiki_search:
|
|
166
|
+
wiki_suggestions = _get_wiki_suggestions_for_error(error_type, message)
|
|
167
|
+
if wiki_suggestions:
|
|
168
|
+
response["wiki_suggestions"] = wiki_suggestions
|
|
169
|
+
response["help_text"] = (
|
|
170
|
+
"💡 Search the Arch Wiki for these topics to find solutions. "
|
|
171
|
+
"Use the search_archwiki tool with these keywords."
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
logger.error(f"{error_type}: {message}")
|
|
175
|
+
|
|
176
|
+
return response
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _get_wiki_suggestions_for_error(error_type: str, message: str) -> list[str]:
|
|
180
|
+
"""
|
|
181
|
+
Generate relevant Arch Wiki search suggestions based on error type.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
error_type: Type of error
|
|
185
|
+
message: Error message
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
List of suggested Wiki search terms
|
|
189
|
+
"""
|
|
190
|
+
suggestions = []
|
|
191
|
+
message_lower = message.lower()
|
|
192
|
+
|
|
193
|
+
# Map error types to Wiki topics
|
|
194
|
+
error_wiki_map = {
|
|
195
|
+
"NotFound": ["Package management", "AUR"],
|
|
196
|
+
"TimeoutError": ["Network configuration", "Mirrors"],
|
|
197
|
+
"HTTPError": ["Network configuration", "Proxy"],
|
|
198
|
+
"CommandNotFound": ["Pacman", "System maintenance"],
|
|
199
|
+
"NotSupported": ["Installation guide", "System requirements"],
|
|
200
|
+
"RateLimitError": ["AUR", "Mirror"],
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Add general suggestions based on error type
|
|
204
|
+
if error_type in error_wiki_map:
|
|
205
|
+
suggestions.extend(error_wiki_map[error_type])
|
|
206
|
+
|
|
207
|
+
# Add context-specific suggestions based on message keywords
|
|
208
|
+
keyword_map = {
|
|
209
|
+
"pacman": ["Pacman", "Pacman/Rosetta"],
|
|
210
|
+
"package": ["Package management", "Official repositories"],
|
|
211
|
+
"dependency": ["Dependency", "Package management"],
|
|
212
|
+
"mirror": ["Mirrors", "Reflector"],
|
|
213
|
+
"network": ["Network configuration", "Systemd-networkd"],
|
|
214
|
+
"update": ["System maintenance", "Pacman#Upgrading packages"],
|
|
215
|
+
"gpg": ["Pacman/Package signing", "GnuPG"],
|
|
216
|
+
"disk": ["File systems", "Partitioning"],
|
|
217
|
+
"boot": ["Boot process", "Arch boot process"],
|
|
218
|
+
"kernel": ["Kernel", "Kernel modules"],
|
|
219
|
+
"driver": ["Kernel modules", "Xorg"],
|
|
220
|
+
"graphics": ["Xorg", "NVIDIA", "AMD"],
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for keyword, topics in keyword_map.items():
|
|
224
|
+
if keyword in message_lower:
|
|
225
|
+
suggestions.extend(topics)
|
|
226
|
+
|
|
227
|
+
# Remove duplicates while preserving order
|
|
228
|
+
seen = set()
|
|
229
|
+
unique_suggestions = []
|
|
230
|
+
for suggestion in suggestions:
|
|
231
|
+
if suggestion not in seen:
|
|
232
|
+
seen.add(suggestion)
|
|
233
|
+
unique_suggestions.append(suggestion)
|
|
234
|
+
|
|
235
|
+
return unique_suggestions[:5] # Limit to top 5 suggestions
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def check_command_exists(command: str) -> bool:
|
|
239
|
+
"""
|
|
240
|
+
Check if a command exists in the system PATH.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
command: Command name to check
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
bool: True if command exists, False otherwise
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
result = os.system(f"which {command} > /dev/null 2>&1")
|
|
250
|
+
return result == 0
|
|
251
|
+
except Exception:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_aur_helper() -> Optional[str]:
|
|
256
|
+
"""
|
|
257
|
+
Detect available AUR helper with priority: paru > yay.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
str: Name of available AUR helper ('paru' or 'yay'), or None if neither exists
|
|
261
|
+
"""
|
|
262
|
+
# Check in priority order
|
|
263
|
+
if check_command_exists("paru"):
|
|
264
|
+
logger.info("Found AUR helper: paru")
|
|
265
|
+
return "paru"
|
|
266
|
+
elif check_command_exists("yay"):
|
|
267
|
+
logger.info("Found AUR helper: yay")
|
|
268
|
+
return "yay"
|
|
269
|
+
else:
|
|
270
|
+
logger.warning("No AUR helper found (paru or yay)")
|
|
271
|
+
return None
|
|
272
|
+
|
arch_ops_server/wiki.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Arch Wiki interface module.
|
|
3
|
+
Provides search and page retrieval via MediaWiki API with BeautifulSoup fallback.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import List, Dict, Any, Optional
|
|
8
|
+
import httpx
|
|
9
|
+
from bs4 import BeautifulSoup
|
|
10
|
+
from markdownify import markdownify as md
|
|
11
|
+
|
|
12
|
+
from .utils import create_error_response
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# Arch Wiki API endpoint
|
|
17
|
+
WIKI_API_URL = "https://wiki.archlinux.org/api.php"
|
|
18
|
+
WIKI_BASE_URL = "https://wiki.archlinux.org"
|
|
19
|
+
|
|
20
|
+
# HTTP client settings
|
|
21
|
+
DEFAULT_TIMEOUT = 10.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def search_wiki(query: str, limit: int = 10) -> Dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Search the Arch Wiki using MediaWiki API.
|
|
27
|
+
|
|
28
|
+
Uses the opensearch action which returns suggestions.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
query: Search term
|
|
32
|
+
limit: Maximum number of results (default: 10)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dict containing search results with titles, snippets, and URLs
|
|
36
|
+
"""
|
|
37
|
+
logger.info(f"Searching Arch Wiki for: {query}")
|
|
38
|
+
|
|
39
|
+
params = {
|
|
40
|
+
"action": "opensearch",
|
|
41
|
+
"search": query,
|
|
42
|
+
"limit": limit,
|
|
43
|
+
"namespace": "0", # Main namespace only
|
|
44
|
+
"format": "json"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
|
|
49
|
+
response = await client.get(WIKI_API_URL, params=params)
|
|
50
|
+
response.raise_for_status()
|
|
51
|
+
|
|
52
|
+
data = response.json()
|
|
53
|
+
|
|
54
|
+
# OpenSearch returns: [query, [titles], [descriptions], [urls]]
|
|
55
|
+
if len(data) >= 4:
|
|
56
|
+
titles = data[1]
|
|
57
|
+
descriptions = data[2]
|
|
58
|
+
urls = data[3]
|
|
59
|
+
|
|
60
|
+
results = [
|
|
61
|
+
{
|
|
62
|
+
"title": title,
|
|
63
|
+
"snippet": desc,
|
|
64
|
+
"url": url
|
|
65
|
+
}
|
|
66
|
+
for title, desc, url in zip(titles, descriptions, urls)
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
logger.info(f"Found {len(results)} results for '{query}'")
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
"query": query,
|
|
73
|
+
"count": len(results),
|
|
74
|
+
"results": results
|
|
75
|
+
}
|
|
76
|
+
else:
|
|
77
|
+
return {
|
|
78
|
+
"query": query,
|
|
79
|
+
"count": 0,
|
|
80
|
+
"results": []
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
except httpx.TimeoutException:
|
|
84
|
+
logger.error(f"Wiki search timed out for query: {query}")
|
|
85
|
+
return create_error_response(
|
|
86
|
+
"TimeoutError",
|
|
87
|
+
f"Arch Wiki search timed out for query: {query}",
|
|
88
|
+
"The Wiki server did not respond in time. Try again later."
|
|
89
|
+
)
|
|
90
|
+
except httpx.HTTPStatusError as e:
|
|
91
|
+
logger.error(f"Wiki search HTTP error: {e}")
|
|
92
|
+
return create_error_response(
|
|
93
|
+
"HTTPError",
|
|
94
|
+
f"Wiki search failed with status {e.response.status_code}",
|
|
95
|
+
str(e)
|
|
96
|
+
)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.error(f"Wiki search failed: {e}")
|
|
99
|
+
return create_error_response(
|
|
100
|
+
"SearchError",
|
|
101
|
+
f"Failed to search Arch Wiki: {str(e)}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def get_wiki_page(title: str, as_markdown: bool = True) -> str:
|
|
106
|
+
"""
|
|
107
|
+
Fetch a Wiki page using MediaWiki API, with scraping fallback.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
title: Page title (e.g., "Installation_guide")
|
|
111
|
+
as_markdown: Convert HTML to Markdown (default: True)
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Page content as Markdown or HTML string
|
|
115
|
+
"""
|
|
116
|
+
logger.info(f"Fetching Wiki page: {title}")
|
|
117
|
+
|
|
118
|
+
# Try MediaWiki API first
|
|
119
|
+
content = await _fetch_via_api(title)
|
|
120
|
+
|
|
121
|
+
# Fallback to scraping if API fails
|
|
122
|
+
if content is None:
|
|
123
|
+
logger.warning(f"API fetch failed for {title}, falling back to scraping")
|
|
124
|
+
content = await _fetch_via_scraping(title)
|
|
125
|
+
|
|
126
|
+
if content is None:
|
|
127
|
+
error_msg = f"Page '{title}' not found or could not be retrieved"
|
|
128
|
+
logger.error(error_msg)
|
|
129
|
+
raise ValueError(error_msg)
|
|
130
|
+
|
|
131
|
+
# Convert to Markdown if requested
|
|
132
|
+
if as_markdown and content:
|
|
133
|
+
try:
|
|
134
|
+
content = md(content, heading_style="ATX", strip=['script', 'style'])
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.warning(f"Markdown conversion failed: {e}, returning HTML")
|
|
137
|
+
|
|
138
|
+
return content
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def _fetch_via_api(title: str) -> Optional[str]:
|
|
142
|
+
"""
|
|
143
|
+
Fetch page content via MediaWiki API.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
title: Page title
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
HTML content or None if failed
|
|
150
|
+
"""
|
|
151
|
+
params = {
|
|
152
|
+
"action": "parse",
|
|
153
|
+
"page": title,
|
|
154
|
+
"format": "json",
|
|
155
|
+
"prop": "text",
|
|
156
|
+
"disableeditsection": "1",
|
|
157
|
+
"disabletoc": "1"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
|
|
162
|
+
response = await client.get(WIKI_API_URL, params=params)
|
|
163
|
+
response.raise_for_status()
|
|
164
|
+
|
|
165
|
+
data = response.json()
|
|
166
|
+
|
|
167
|
+
# Check for errors in response
|
|
168
|
+
if "error" in data:
|
|
169
|
+
logger.warning(f"API error: {data['error'].get('info', 'Unknown error')}")
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
# Extract HTML content
|
|
173
|
+
if "parse" in data and "text" in data["parse"]:
|
|
174
|
+
html_content = data["parse"]["text"]["*"]
|
|
175
|
+
logger.info(f"Successfully fetched {title} via API")
|
|
176
|
+
return html_content
|
|
177
|
+
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.warning(f"API fetch failed for {title}: {e}")
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def _fetch_via_scraping(title: str) -> Optional[str]:
|
|
186
|
+
"""
|
|
187
|
+
Fetch page content via direct HTTP scraping (fallback).
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
title: Page title
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
HTML content or None if failed
|
|
194
|
+
"""
|
|
195
|
+
# Construct URL
|
|
196
|
+
url = f"{WIKI_BASE_URL}/title/{title}"
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT) as client:
|
|
200
|
+
response = await client.get(url, follow_redirects=True)
|
|
201
|
+
response.raise_for_status()
|
|
202
|
+
|
|
203
|
+
# Parse HTML
|
|
204
|
+
soup = BeautifulSoup(response.text, 'lxml')
|
|
205
|
+
|
|
206
|
+
# Find main content div
|
|
207
|
+
content_div = soup.find('div', {'id': 'bodyContent'})
|
|
208
|
+
|
|
209
|
+
if content_div:
|
|
210
|
+
# Remove unnecessary elements
|
|
211
|
+
for element in content_div.find_all(['script', 'style', 'nav']):
|
|
212
|
+
element.decompose()
|
|
213
|
+
|
|
214
|
+
logger.info(f"Successfully scraped {title}")
|
|
215
|
+
return str(content_div)
|
|
216
|
+
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
except httpx.HTTPStatusError as e:
|
|
220
|
+
if e.response.status_code == 404:
|
|
221
|
+
logger.error(f"Page not found: {title}")
|
|
222
|
+
else:
|
|
223
|
+
logger.error(f"HTTP error scraping {title}: {e}")
|
|
224
|
+
return None
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Scraping failed for {title}: {e}")
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def get_wiki_page_as_text(title: str) -> str:
|
|
231
|
+
"""
|
|
232
|
+
Convenience wrapper to get Wiki page as clean Markdown text.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
title: Page title
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Markdown content
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
ValueError: If page cannot be retrieved
|
|
242
|
+
"""
|
|
243
|
+
return await get_wiki_page(title, as_markdown=True)
|
|
244
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: arch-ops-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server bridging AI assistants with Arch Linux ecosystem (Wiki, AUR, official repos)
|
|
5
|
+
Keywords: arch-linux,mcp,model-context-protocol,aur,pacman,wiki,ai-assistant
|
|
6
|
+
Author: Nihal
|
|
7
|
+
Author-email: Nihal <2tv8xupqg@mozmail.com>
|
|
8
|
+
License: GPL-3.0
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: System :: Archiving :: Packaging
|
|
18
|
+
Classifier: Topic :: System :: Systems Administration
|
|
19
|
+
Requires-Dist: mcp>=1.0.0
|
|
20
|
+
Requires-Dist: httpx>=0.27.0
|
|
21
|
+
Requires-Dist: beautifulsoup4>=4.12.0
|
|
22
|
+
Requires-Dist: lxml>=5.0.0
|
|
23
|
+
Requires-Dist: markdownify>=0.12.0
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Arch Linux MCP Server
|
|
28
|
+
|
|
29
|
+
**Disclaimer:** Unofficial community project, not affiliated with Arch Linux.
|
|
30
|
+
|
|
31
|
+
A [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that bridges AI assistants with the Arch Linux ecosystem. Enables intelligent, safe, and efficient access to the Arch Wiki, AUR, and official repositories for AI-assisted Arch Linux usage on Arch and non-Arch systems.
|
|
32
|
+
|
|
33
|
+
Leverage AI to get output for digestible, structured results that are ready for follow up questions and actions.
|
|
34
|
+
|
|
35
|
+
📖 [Complete Documentation with Comfy Guides](https://nxk.mintlify.app/arch-mcp)
|
|
36
|
+
|
|
37
|
+
## Sneak Peak into what's available
|
|
38
|
+
|
|
39
|
+
### Resources (URI-based Access)
|
|
40
|
+
|
|
41
|
+
Direct access to Arch ecosystem data via custom URI schemes:
|
|
42
|
+
|
|
43
|
+
| URI Scheme | Example | Returns |
|
|
44
|
+
|------------|---------|---------|
|
|
45
|
+
| `archwiki://` | `archwiki://Installation_guide` | Markdown-formatted Wiki page |
|
|
46
|
+
| `aur://*/pkgbuild` | `aur://yay/pkgbuild` | Raw PKGBUILD with safety analysis |
|
|
47
|
+
| `aur://*/info` | `aur://yay/info` | AUR package metadata (votes, maintainer, dates) |
|
|
48
|
+
| `archrepo://` | `archrepo://vim` | Official repository package details |
|
|
49
|
+
| `pacman://installed` | `pacman://installed` | System installed packages list (Arch only) |
|
|
50
|
+
|
|
51
|
+
### Tools (Executable Functions)
|
|
52
|
+
|
|
53
|
+
| Category | Tool | Description | Key Features |
|
|
54
|
+
|----------|------|-------------|--------------|
|
|
55
|
+
| **Search** | `search_archwiki` | Query Arch Wiki documentation | Ranked results, keyword extraction |
|
|
56
|
+
| | `search_aur` | Search AUR packages | Smart ranking: relevance/votes/popularity/modified |
|
|
57
|
+
| | `get_official_package_info` | Lookup official packages | Hybrid local/remote, detailed metadata |
|
|
58
|
+
| **System** | `check_updates_dry_run` | Check for updates (Arch only) | Read-only, safe, requires pacman-contrib |
|
|
59
|
+
| **Installation** | `install_package_secure` | Secure package installation | Auto security checks, blocks malicious packages, uses paru/yay |
|
|
60
|
+
| **Security** | `analyze_pkgbuild_safety` | Comprehensive PKGBUILD analysis | Detects: malicious commands based on 50+ red flags |
|
|
61
|
+
| | `analyze_package_metadata_risk` | Package trust evaluation | Analyzes: votes, maintainer, age, updates, trust scoring |
|
|
62
|
+
|
|
63
|
+
### Prompts (Guided Workflows)
|
|
64
|
+
|
|
65
|
+
| Prompt | Purpose | Workflow |
|
|
66
|
+
|--------|---------|----------|
|
|
67
|
+
| `troubleshoot_issue` | Diagnose system errors | Extract keywords → Search Wiki → Context-aware suggestions |
|
|
68
|
+
| `audit_aur_package` | Pre-installation safety audit | Fetch metadata → Analyze PKGBUILD → Security recommendations |
|
|
69
|
+
| `analyze_dependencies` | Installation planning | Check repos → Map dependencies → Suggest install order |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
### Prerequisites
|
|
76
|
+
- Python 3.11+
|
|
77
|
+
- [uv](https://github.com/astral-sh/uv) (recommended) or pip
|
|
78
|
+
|
|
79
|
+
### Quick Install with `uvx`
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uvx arch-ops-server
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Configuration
|
|
88
|
+
|
|
89
|
+
Claude / Cursor / Any MCP client that supports STDIO transport
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"mcpServers": {
|
|
94
|
+
"arch-ops": {
|
|
95
|
+
"command": "uvx",
|
|
96
|
+
"args": ["arch-ops-server"]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
[GPL-3.0-only](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
Built with ❤️ for the Arch Linux community
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
arch_ops_server/__init__.py,sha256=m0KiRT4iLqY5eyP3Ul-anwQVdpjTghWWW1QOqk6n1NI,1906
|
|
2
|
+
arch_ops_server/aur.py,sha256=_BoPsKyBPa2KWi9i1J_RSsqUT53wU03guS0V1sasOXg,46328
|
|
3
|
+
arch_ops_server/pacman.py,sha256=nD1B8KgHuwaxHtcO4sd1WpiFcpocjD6yxs1JtVkTjPU,9862
|
|
4
|
+
arch_ops_server/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
arch_ops_server/server.py,sha256=D_8uypCGmWgTjw5VThrsBY_XxrDjHmdVJT6D39Be-Sw,25212
|
|
6
|
+
arch_ops_server/utils.py,sha256=-v47tnfxRP-IqvKeqhpAIYVIPhTIh5DMe9jWvCMNRGM,8145
|
|
7
|
+
arch_ops_server/wiki.py,sha256=P6znxzV2e9JVUq8yyD0e9pP0Fm7EkS9vmDb2HUdwQpc,7303
|
|
8
|
+
arch_ops_server-0.1.0.dist-info/WHEEL,sha256=k57ZwB-NkeM_6AsPnuOHv5gI5KM5kPD6Vx85WmGEcI0,78
|
|
9
|
+
arch_ops_server-0.1.0.dist-info/entry_points.txt,sha256=nD6HtiLT-Xh1b63_LGcYNEjHqVlal7I2d5jeFJMtfiU,63
|
|
10
|
+
arch_ops_server-0.1.0.dist-info/METADATA,sha256=tEmFaZ5X5emaz6rLExwi-QieYk8StI-N0sz0ezSacnk,4183
|
|
11
|
+
arch_ops_server-0.1.0.dist-info/RECORD,,
|