Add some more MCP tools

One for listing the repos, and one for searching packages
This commit is contained in:
Christoph Reiter 2025-07-07 20:07:25 +02:00
parent 1c703c66d7
commit 809daefa3f
4 changed files with 107 additions and 40 deletions

View File

@ -813,3 +813,45 @@ class SrcInfoPackage:
state = AppState()
def find_packages(query: str, qtype: str) -> list[Package | Source]:
if qtype not in ["pkg", "binpkg"]:
qtype = "pkg"
parts = query.split()
parts_lower = [p.lower() for p in parts]
res_pkg: list[tuple[float, Package | Source]] = []
def get_score(name: str, parts: list[str]) -> float:
score = 0.0
for part in parts:
if part not in name:
return -1
score += name.count(part) * len(part) / len(name)
return score
if not query:
pass
elif qtype == "pkg":
for s in state.sources.values():
score = get_score(s.realname.lower(), parts_lower)
if score >= 0:
res_pkg.append((score, s))
continue
score = get_score(s.name.lower(), parts_lower)
if score >= 0:
res_pkg.append((score, s))
res_pkg.sort(key=lambda e: (-e[0], e[1].name.lower()))
elif qtype == "binpkg":
for s in state.sources.values():
for sub in s.packages.values():
score = get_score(sub.realname.lower(), parts_lower)
if score >= 0:
res_pkg.append((score, sub))
continue
score = get_score(sub.name.lower(), parts_lower)
if score >= 0:
res_pkg.append((score, sub))
res_pkg.sort(key=lambda e: (-e[0], e[1].name.lower()))
return [r[1] for r in res_pkg]

View File

@ -1,8 +1,10 @@
from mcp.server.fastmcp import FastMCP
from .utils import vercmp
from pydantic import BaseModel, Field
from .appstate import get_repositories, find_packages, Source
mcpapp = FastMCP(name="MathServer", stateless_http=True, json_response=False)
mcpapp = FastMCP(name="MSYS2Server", stateless_http=True, json_response=False)
@mcpapp.tool()
@ -16,3 +18,60 @@ def msys2_vercmp(versionA: str, versionB: str) -> int:
"""
return vercmp(versionA, versionB)
class MCPRepository(BaseModel):
"""A MSYS2 repository"""
name: str = Field(..., description="Name of the repository")
pacman_url: str = Field(..., description="A full URL to a location where the database, packages, and signatures for this repository can be found.")
src_url: str = Field(..., description="Git source URL of the repository, where the PKGBUILD and other source files can be found.")
@mcpapp.tool()
def msys2_list_repositories() -> list[MCPRepository]:
"""Returns a list of MSYS2 repositories"""
res = []
for repo in get_repositories():
res.append(MCPRepository(name=repo.name, pacman_url=repo.url, src_url=repo.src_url))
return res
class MCPPackage(BaseModel):
"""A MSYS2 package"""
name: str = Field(..., description="Name of the package")
version: str = Field(..., description="Version of the package")
description: str = Field(..., description="Description of the package")
repository: str = Field(..., description="Repository where the package is located")
class MCPBasePackage(BaseModel):
"""A base package in MSYS2"""
name: str = Field(..., description="Name of the base package")
description: str = Field(..., description="Description of the base package")
packages: list[MCPPackage] = Field(default_factory=list, description="List of packages that belong to this base package")
@mcpapp.tool()
def msys2_search_base_packages(query: str, limit: int = 25) -> list[MCPBasePackage]:
"""Find MSYS2 base packages
Args:
query: The search query to find base packages.
limit: The maximum number of results to return (default is 25).
Returns:
A list of package names that match the query, sorted by relevance.
"""
res = []
for src in find_packages(query, "pkg")[:limit]:
assert isinstance(src, Source)
pkgres = []
for pkg in src.packages.values():
pkgres.append(MCPPackage(name=pkg.name, version=pkg.version, description=pkg.desc, repository=pkg.repo))
res.append(MCPBasePackage(name=src.name, description=src.desc, packages=pkgres))
return res

View File

@ -61,7 +61,7 @@
</tr>
</thead>
<tbody>
{% for score, s in results %}
{% for s in results %}
<tr>
<td><a href="{{ url_for('base', base_name=s.name) }}">{{ s.name }}</a></td>
<td>{{ s.version }}</td>
@ -95,7 +95,7 @@
</tr>
</thead>
<tbody>
{% for score, p in results %}
{% for p in results %}
<tr>
<td><a href="{{ package_url(p) }}">{{ p.name }}</td>
<td>{{ p.version }}</td>

View File

@ -21,7 +21,7 @@ from fastapi_etag import Etag
from fastapi.staticfiles import StaticFiles
from fastapi_etag import add_exception_handler as add_etag_exception_handler
from .appstate import state, get_repositories, Package, Source, DepType, SrcInfoPackage, get_base_group_name, Vulnerability, Severity, PackageKey
from .appstate import state, get_repositories, Package, Source, DepType, SrcInfoPackage, get_base_group_name, Vulnerability, Severity, PackageKey, find_packages
from .utils import extract_upstream_version, version_is_newer_than
router = APIRouter(default_response_class=HTMLResponse)
@ -730,44 +730,10 @@ async def search(request: Request, response: Response, q: str = "", t: str = "")
if qtype not in ["pkg", "binpkg"]:
qtype = "pkg"
parts = query.split()
parts_lower = [p.lower() for p in parts]
res_pkg: list[tuple[float, Package | Source]] = []
def get_score(name: str, parts: list[str]) -> float:
score = 0.0
for part in parts:
if part not in name:
return -1
score += name.count(part) * len(part) / len(name)
return score
if not query:
pass
elif qtype == "pkg":
for s in state.sources.values():
score = get_score(s.realname.lower(), parts_lower)
if score >= 0:
res_pkg.append((score, s))
continue
score = get_score(s.name.lower(), parts_lower)
if score >= 0:
res_pkg.append((score, s))
res_pkg.sort(key=lambda e: (-e[0], e[1].name.lower()))
elif qtype == "binpkg":
for s in state.sources.values():
for sub in s.packages.values():
score = get_score(sub.realname.lower(), parts_lower)
if score >= 0:
res_pkg.append((score, sub))
continue
score = get_score(sub.name.lower(), parts_lower)
if score >= 0:
res_pkg.append((score, sub))
res_pkg.sort(key=lambda e: (-e[0], e[1].name.lower()))
results = find_packages(query, qtype)
return templates.TemplateResponse(request, "search.html", {
"results": res_pkg,
"results": results,
"query": query,
"qtype": qtype,
}, headers=dict(response.headers))