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:
1 parent
465a365d89
commit
1d223e6799
7 files changed
+1798
-3
No files matched your search
@@ -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-"
|
||||
Reference in new issue
Block a user