feat: add the aegis-gitea-mcp setup command

Verifies the Gitea instance, token and scopes, registers or reuses the aegis-gitea-mcp OAuth app, generates missing secrets and writes a 0600 env file from the template. Prints client configs and deployment snippets without any secret value.
This commit is contained in:
Latte committed 2026-10-14 15:50:02 +02:00
1 parent 465a365d89
commit 1d223e6799
7 files changed
+1798 -3

No files matched your search

+143
View File
@@ -0,0 +1,143 @@
"""Client and deployment snippets printed by ``aegis-gitea-mcp setup``.
Every function returns plain text and takes only non-secret inputs. Secrets are
always shown as placeholders such as ``<your-gitea-token>``, so these templates
can be printed, logged and reused in the documentation without leaking anything.
"""
from __future__ import annotations
import json
import shlex
from urllib.parse import urlsplit
#: Name used for the server in client configurations.
SERVER_NAME = "aegis-gitea"
#: Container image used by the docker-compose snippet.
DOCKER_IMAGE = "git.hiddenden.cafe/hiddenden/aegisgitea-mcp:dev"
TOKEN_PLACEHOLDER = "<your-gitea-token>" # noqa: S105
BEARER_PLACEHOLDER = "<MCP_BEARER_TOKEN>"
#: Loopback base URL used when no public base URL is configured.
LOCAL_BASE_URL = "http://127.0.0.1:8080"
def claude_code_stdio(gitea_url: str) -> str:
"""Return the ``claude mcp add`` command for the local stdio server."""
return (
"claude mcp add --transport stdio "
f"--env GITEA_URL={gitea_url} --env GITEA_TOKEN={TOKEN_PLACEHOLDER} "
f"{SERVER_NAME} -- uvx aegis-gitea-mcp"
)
def mcp_servers_json(gitea_url: str) -> str:
"""Return a generic ``mcpServers`` JSON block (Claude Desktop, Cursor, and similar)."""
block = {
"mcpServers": {
SERVER_NAME: {
"command": "uvx",
"args": ["aegis-gitea-mcp"],
"env": {"GITEA_URL": gitea_url, "GITEA_TOKEN": TOKEN_PLACEHOLDER},
}
}
}
return json.dumps(block, indent=2)
def mcp_endpoint(base_url: str, mcp_path: str = "/mcp") -> str:
"""Return the full MCP endpoint URL for a base URL."""
return f"{base_url.rstrip('/')}{mcp_path}"
def claude_code_http(base_url: str, mcp_path: str = "/mcp") -> str:
"""Return the ``claude mcp add`` command for the streamable-HTTP endpoint."""
return (
f"claude mcp add --transport http {SERVER_NAME} {mcp_endpoint(base_url, mcp_path)} "
f'--header "Authorization: Bearer {BEARER_PLACEHOLDER}"'
)
def claude_ai_connector(base_url: str, mcp_path: str = "/mcp", *, oauth: bool) -> str:
"""Return setup instructions for a claude.ai custom connector."""
lines = [
"Settings > Connectors > Add custom connector",
f" URL: {mcp_endpoint(base_url, mcp_path)}",
]
if oauth:
lines.append(
" Authentication: leave the OAuth client fields empty. Claude registers itself "
"(dynamic client registration) and sends you to Gitea to sign in."
)
else:
lines.append(
" Authentication: OAuth is off. Use a static bearer token where your plan "
"supports it, or enable OAuth and re-run setup."
)
return "\n".join(lines)
def le_chat_connector(base_url: str, mcp_path: str = "/mcp", *, oauth: bool) -> str:
"""Return setup instructions for a Mistral Le Chat custom connector."""
lines = [
"Intelligence > Connectors > Add Connector > Custom MCP Connector",
f" URL: {mcp_endpoint(base_url, mcp_path)} (HTTPS with a valid certificate)",
f" Authentication: HTTP Bearer Token, with the {BEARER_PLACEHOLDER} value",
]
if oauth:
lines.append(
" or OAuth 2.1 with dynamic client registration (users sign in with their "
"own Gitea account)."
)
return "\n".join(lines)
def docker_compose(port: int = 8080) -> str:
"""Return a hardened docker-compose service for the HTTP transport."""
return f"""\
services:
aegis-gitea-mcp:
image: {DOCKER_IMAGE}
restart: unless-stopped
env_file: .env
environment:
MCP_TRANSPORT: http
MCP_HOST: 0.0.0.0 # allowed because authentication is configured
DATA_DIR: /var/lib/aegis-mcp
ports:
- "127.0.0.1:{port}:8080"
volumes:
- aegis-mcp-data:/var/lib/aegis-mcp
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
volumes:
aegis-mcp-data:
"""
def caddy_snippet(public_base_url: str, port: int = 8080) -> str:
"""Return a Caddyfile site block that terminates TLS and proxies to the container."""
host = urlsplit(public_base_url).netloc or "gitea-mcp.example.com"
return f"""\
{host} {{
reverse_proxy 127.0.0.1:{port} {{
flush_interval -1
}}
}}
"""
NGINX_NOTE = (
"nginx: inside the proxying location block set `proxy_buffering off;` so "
"streamed responses are not held back."
)
def read_secret_command(env_file: str, key: str) -> str:
"""Return a shell command that prints one value of the env file."""
return f"grep {shlex.quote(f'^{key}=')} {shlex.quote(env_file)} | cut -d= -f2-"