commit 7fd711a83f9733aac41075b97a41b7330c7b33d6 Author: root Date: Wed Jul 15 17:53:14 2026 -0400 MCP server source diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..733a302 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.13-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY server.py ./ +ENV PYTHONUNBUFFERED=1 +EXPOSE 8903 +CMD ["python3", "server.py"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5bae337 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +fastmcp>=3.0.0 +httpx>=0.27.0 diff --git a/server.py b/server.py new file mode 100644 index 0000000..c06d762 --- /dev/null +++ b/server.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Git/Gitea MCP Server — Browse repos via Gitea API.""" +from fastmcp import FastMCP +import httpx, os, base64 + +GITEA_URL = os.getenv('GITEA_URL', 'https://git.itpropartner.com') +API = f'{GITEA_URL}/api/v1' + +mcp = FastMCP('Git/Gitea', version='1.0.0') + +async def _get(path: str): + async with httpx.AsyncClient(verify=False, timeout=15) as c: + r = await c.get(f'{API}{path}') + r.raise_for_status() + return r.json() + +@mcp.tool(description='List all public repos on git.itpropartner.com') +async def list_repos() -> str: + """List all Gitea repos.""" + repos = await _get('/repos/search?limit=50&q=&template=false') + if isinstance(repos, dict) and 'data' in repos: + repos = repos['data'] + if not repos: + return 'No repos found. Gitea may need initial setup at https://git.itpropartner.com' + lines = [f'{len(repos)} repos:'] + for r in repos[:30]: + lines.append(f' {r["full_name"]} — {r.get("description","")} ({r.get("stars_count",0)}★)') + return '\n'.join(lines) + +@mcp.tool(description='Search Gitea repos by name or description') +async def search_repos(query: str) -> str: + """Search repos.""" + repos = await _get(f'/repos/search?q={query}&limit=20') + if isinstance(repos, dict) and 'data' in repos: + repos = repos['data'] + if not repos: + return f'No repos matching "{query}"' + lines = [f'Results for "{query}":'] + for r in repos[:20]: + lines.append(f' {r["full_name"]} — {r.get("description","")}') + return '\n'.join(lines) + +@mcp.tool(description='Browse files and folders in a Gitea repo') +async def browse_repo(owner: str, repo: str, path: str = '') -> str: + """List contents of a repo directory.""" + contents = await _get(f'/repos/{owner}/{repo}/contents/{path}') + if not contents: + return f'Empty: {owner}/{repo}/{path}' + lines = [f'{owner}/{repo}/{path or "/"}:'] + for item in contents[:50]: + typ = '[DIR]' if item['type'] == 'dir' else f'[{item["type"]}]' + lines.append(f' {typ} {item["name"]} ({item.get("size",0):,} bytes)') + return '\n'.join(lines) + +@mcp.tool(description='Read a file from a Gitea repo') +async def read_file(owner: str, repo: str, path: str) -> str: + """Read file contents.""" + data = await _get(f'/repos/{owner}/{repo}/contents/{path}') + if 'content' in data: + content = base64.b64decode(data['content']).decode('utf-8', errors='replace') + return f'{owner}/{repo}/{path} ({data["size"]:,} bytes):\n\n{content[:10000]}' + return f'Could not read {owner}/{repo}/{path}' + +if __name__ == '__main__': + mcp.run(transport='http', host='0.0.0.0', port=8903, path='/mcp')