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.
144 lines
4.6 KiB
Python
144 lines
4.6 KiB
Python
"""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-"
|