Dev #72
No files matched your search
@@ -145,9 +145,12 @@ Gitea token, `<MCP_BEARER_TOKEN>` is the value from the env file.
|
|||||||
Claude Code:
|
Claude Code:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
claude mcp add --transport stdio --env GITEA_URL=https://git.example.com --env GITEA_TOKEN=<your-gitea-token> aegis-gitea -- uvx aegis-gitea-mcp
|
claude mcp add --env GITEA_URL=https://git.example.com --env GITEA_TOKEN=<your-gitea-token> --transport stdio aegis-gitea -- uvx --index https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/ aegis-gitea-mcp
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Keep `--transport stdio` and the server name after the `--env` options: `--env` accepts several
|
||||||
|
values, so an option placed before the name would swallow it.
|
||||||
|
|
||||||
Claude Desktop, Cursor and other local clients that read an `mcpServers` block:
|
Claude Desktop, Cursor and other local clients that read an `mcpServers` block:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -155,7 +158,7 @@ Claude Desktop, Cursor and other local clients that read an `mcpServers` block:
|
|||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"aegis-gitea": {
|
"aegis-gitea": {
|
||||||
"command": "uvx",
|
"command": "uvx",
|
||||||
"args": ["aegis-gitea-mcp"],
|
"args": ["--index", "https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/", "aegis-gitea-mcp"],
|
||||||
"env": {
|
"env": {
|
||||||
"GITEA_URL": "https://git.example.com",
|
"GITEA_URL": "https://git.example.com",
|
||||||
"GITEA_TOKEN": "<your-gitea-token>"
|
"GITEA_TOKEN": "<your-gitea-token>"
|
||||||
@@ -165,9 +168,16 @@ Claude Desktop, Cursor and other local clients that read an `mcpServers` block:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`uvx` installs the package from your configured index (see [packaging.md](docs/packaging.md)
|
Always pass the `--index` of the project's registry. The name `aegis-gitea-mcp` is not
|
||||||
for the Gitea registry). To run from a clone instead, use `uv` as the command with the
|
registered on public PyPI, so a bare `uvx aegis-gitea-mcp` would resolve there, and whoever
|
||||||
arguments `--directory /path/to/AegisGitea-MCP run aegis-gitea-mcp`.
|
registers that name would receive your Gitea token through the environment. The owner should
|
||||||
|
reserve the name on PyPI to close that gap (see [packaging.md](docs/packaging.md)). To run from
|
||||||
|
a clone instead, use `uv` as the command with the arguments
|
||||||
|
`run --directory /path/to/AegisGitea-MCP aegis-gitea-mcp serve`, or on the command line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add --env GITEA_URL=https://git.example.com --env GITEA_TOKEN=<your-gitea-token> --transport stdio aegis-gitea -- uv run --directory /path/to/AegisGitea-MCP aegis-gitea-mcp serve
|
||||||
|
```
|
||||||
|
|
||||||
### Remote streamable HTTP
|
### Remote streamable HTTP
|
||||||
|
|
||||||
@@ -184,7 +194,8 @@ claude.ai custom connector: Settings, Connectors, Add custom connector, URL
|
|||||||
Claude registers itself (dynamic client registration) and sends you to Gitea to sign in.
|
Claude registers itself (dynamic client registration) and sends you to Gitea to sign in.
|
||||||
|
|
||||||
Mistral Le Chat custom connector: Intelligence, Connectors, Add Connector, Custom MCP
|
Mistral Le Chat custom connector: Intelligence, Connectors, Add Connector, Custom MCP
|
||||||
Connector, URL `https://gitea-mcp.example.com/mcp` (HTTPS with a valid certificate).
|
Connector, URL `https://gitea-mcp.example.com/mcp` (HTTPS with a valid certificate). Adding
|
||||||
|
connectors in Le Chat may require workspace admin rights.
|
||||||
Authentication is either HTTP Bearer Token with the `MCP_BEARER_TOKEN` value, or OAuth 2.1 with
|
Authentication is either HTTP Bearer Token with the `MCP_BEARER_TOKEN` value, or OAuth 2.1 with
|
||||||
dynamic client registration so users sign in with their own Gitea account.
|
dynamic client registration so users sign in with their own Gitea account.
|
||||||
|
|
||||||
@@ -267,7 +278,7 @@ you with mode 0600 (what `setup` writes); any other `./.env` is ignored with a w
|
|||||||
| `MCP_HOST`, `MCP_PORT`, `MCP_PATH` | `127.0.0.1`, `8080`, `/mcp` | HTTP bind and endpoint path |
|
| `MCP_HOST`, `MCP_PORT`, `MCP_PATH` | `127.0.0.1`, `8080`, `/mcp` | HTTP bind and endpoint path |
|
||||||
| `MCP_BEARER_TOKEN` | unset | Static bearer for HTTP, at least 32 characters |
|
| `MCP_BEARER_TOKEN` | unset | Static bearer for HTTP, at least 32 characters |
|
||||||
| `OAUTH_ENABLED` | `false` | Per-user OAuth on HTTP |
|
| `OAUTH_ENABLED` | `false` | Per-user OAuth on HTTP |
|
||||||
| `READ_ONLY` | `false` | Expose only read-only operations |
|
| `READ_ONLY` | `false` | Expose only operations annotated read-only |
|
||||||
| `TOOLSETS` | `all` | Comma separated domains to expose |
|
| `TOOLSETS` | `all` | Comma separated domains to expose |
|
||||||
| `DISABLED_TOOLS` | empty | Tool names to hide |
|
| `DISABLED_TOOLS` | empty | Tool names to hide |
|
||||||
| `SECRET_DETECTION_MODE` | `mask` | `off`, `mask` or `block` |
|
| `SECRET_DETECTION_MODE` | `mask` | `off`, `mask` or `block` |
|
||||||
@@ -277,8 +288,10 @@ Every variable, its default and its validation rules are in
|
|||||||
|
|
||||||
## What is config-gated, and what is impossible
|
## What is config-gated, and what is impossible
|
||||||
|
|
||||||
Config-gated, all optional: `READ_ONLY` (only GET operations, and `gitea_api_request` accepts
|
Config-gated, all optional: `READ_ONLY` (exposes only the operations annotated read-only, which
|
||||||
only GET), `TOOLSETS` (domains), `DISABLED_TOOLS` (single tools, an unknown name is a startup
|
are the GET operations plus a few POST endpoints that do not change anything, such as the
|
||||||
|
markdown and markup render endpoints and the file-contents query; `gitea_api_request` stays
|
||||||
|
listed but accepts only GET), `TOOLSETS` (domains), `DISABLED_TOOLS` (single tools, an unknown name is a startup
|
||||||
error), and `SECRET_DETECTION_MODE` (`mask` replaces secret-like strings in results, `block`
|
error), and `SECRET_DETECTION_MODE` (`mask` replaces secret-like strings in results, `block`
|
||||||
withholds a result that contains one, `off` disables it; exact redaction of the server's own
|
withholds a result that contains one, `off` disables it; exact redaction of the server's own
|
||||||
tokens always applies). A filtered tool is neither listed nor callable.
|
tokens always applies). A filtered tool is neither listed nor callable.
|
||||||
@@ -288,6 +301,22 @@ writes to the instance configuration, and instance-wide OAuth applications (UI o
|
|||||||
registers its OAuth application as a user application of the admin). `gitea_api_request` reaches
|
registers its OAuth application as a user application of the admin). `gitea_api_request` reaches
|
||||||
anything newer than the pinned spec once Gitea ships it.
|
anything newer than the pinned spec once Gitea ships it.
|
||||||
|
|
||||||
|
## Known limitations
|
||||||
|
|
||||||
|
A token with the `all` scope does not make every tool work. Check
|
||||||
|
[docs/integration-coverage.md](docs/integration-coverage.md), generated by running every tool
|
||||||
|
against a live Gitea 28.1.0, for the per-tool outcome.
|
||||||
|
|
||||||
|
- `gitea_user_create_token`, `gitea_user_get_tokens` and `gitea_user_delete_access_token` need
|
||||||
|
basic authentication with the account password (a Gitea API rule). The server always sends
|
||||||
|
token authentication, so these three return 401 through it. Create tokens in the Gitea UI.
|
||||||
|
- Some operations depend on instance state and answer 404 or an error until it exists: the
|
||||||
|
commit signing keys (`gitea_misc_get_signing_key`, `gitea_repo_signing_key` and their SSH
|
||||||
|
variants) when the instance has none, user badges (Gitea cannot create badges through the
|
||||||
|
API), unadopted repositories, and similar. The coverage report lists them with the reason.
|
||||||
|
- Tools that depend on a runner, such as workflow runs and jobs, only return data once an
|
||||||
|
Actions runner is registered.
|
||||||
|
|
||||||
## Result and error format
|
## Result and error format
|
||||||
|
|
||||||
A successful call returns one text item with JSON (see
|
A successful call returns one text item with JSON (see
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ listed nor callable.
|
|||||||
|
|
||||||
| Variable | Default | Rules |
|
| Variable | Default | Rules |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `READ_ONLY` | `false` | Expose only read-only (GET) operations. `gitea_api_request` stays listed but accepts only GET. |
|
| `READ_ONLY` | `false` | Expose only operations annotated read-only (GET plus a few non-mutating POST endpoints such as the render endpoints). `gitea_api_request` stays listed but accepts only GET. |
|
||||||
| `TOOLSETS` | `all` | Comma separated domains: `admin`, `repo`, `issue`, `pull`, `release`, `wiki`, `org`, `team`, `user`, `actions`, `hook`, `package`, `notification`, `misc`, or `all`. Unknown domains are rejected. `gitea_api_request` belongs to `misc`. |
|
| `TOOLSETS` | `all` | Comma separated domains: `admin`, `repo`, `issue`, `pull`, `release`, `wiki`, `org`, `team`, `user`, `actions`, `hook`, `package`, `notification`, `misc`, or `all`. Unknown domains are rejected. `gitea_api_request` belongs to `misc`. |
|
||||||
| `DISABLED_TOOLS` | empty | Exact tool names to hide, lowercased on load. An unknown name is a startup error that lists the bad names. |
|
| `DISABLED_TOOLS` | empty | Exact tool names to hide, lowercased on load. An unknown name is a startup error that lists the bad names. |
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -67,7 +67,7 @@ Check it: `curl http://127.0.0.1:8080/health`.
|
|||||||
|
|
||||||
Terminate TLS at a reverse proxy. `PUBLIC_BASE_URL` must be the public https URL (it is required
|
Terminate TLS at a reverse proxy. `PUBLIC_BASE_URL` must be the public https URL (it is required
|
||||||
for OAuth, and http is only accepted for loopback hosts). Mistral Le Chat needs a valid public
|
for OAuth, and http is only accepted for loopback hosts). Mistral Le Chat needs a valid public
|
||||||
certificate.
|
certificate, and adding a connector there may require workspace admin rights.
|
||||||
|
|
||||||
Caddy:
|
Caddy:
|
||||||
|
|
||||||
|
|||||||
@@ -96,10 +96,14 @@ make validate-audit
|
|||||||
With `uvx`, no checkout is needed:
|
With `uvx`, no checkout is needed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
GITEA_URL=https://git.example.com GITEA_TOKEN=<your-gitea-token> uvx aegis-gitea-mcp
|
GITEA_URL=https://git.example.com GITEA_TOKEN=<your-gitea-token> \
|
||||||
|
uvx --index https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/ aegis-gitea-mcp
|
||||||
```
|
```
|
||||||
|
|
||||||
The package comes from the Gitea package registry, see [packaging.md](packaging.md).
|
The package comes from the Gitea package registry, see [packaging.md](packaging.md). Always pass
|
||||||
|
the `--index`: the name is not registered on public PyPI, and a bare `uvx aegis-gitea-mcp` would
|
||||||
|
resolve there. From a clone use
|
||||||
|
`uv run --directory /path/to/AegisGitea-MCP aegis-gitea-mcp serve`.
|
||||||
|
|
||||||
## Where state lives
|
## Where state lives
|
||||||
|
|
||||||
|
|||||||
+30
-12
@@ -44,28 +44,46 @@ Dependencies are pinned in `uv.lock`; `uv sync` reproduces the environment.
|
|||||||
|
|
||||||
## Install from the Gitea registry
|
## Install from the Gitea registry
|
||||||
|
|
||||||
|
Index URL: `https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/`
|
||||||
|
|
||||||
Stable:
|
Stable:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv pip install \
|
# run it without installing into an environment (recommended):
|
||||||
--extra-index-url https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/ \
|
|
||||||
aegis-gitea-mcp
|
|
||||||
|
|
||||||
# or run it without installing into an environment:
|
|
||||||
uvx --index https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/ aegis-gitea-mcp
|
uvx --index https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/ aegis-gitea-mcp
|
||||||
|
|
||||||
|
# or install it into an environment:
|
||||||
|
uv pip install --index https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/ aegis-gitea-mcp
|
||||||
```
|
```
|
||||||
|
|
||||||
Dev (newest pre-release build):
|
Dev (newest pre-release build, package `aegis-gitea-mcp-dev`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv pip install --pre \
|
uvx --index https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/ --prerelease allow aegis-gitea-mcp-dev
|
||||||
--extra-index-url https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/ \
|
|
||||||
aegis-gitea-mcp-dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The registry holds only this project's packages and does not proxy PyPI, so it is added as an
|
The registry holds only this project's packages and does not proxy PyPI, so it is added with
|
||||||
extra index and the dependencies (`mcp`, `httpx`, ...) still come from PyPI. With `pip` use
|
`--index` and the dependencies (`mcp`, `httpx`, ...) still come from PyPI. uv's default index
|
||||||
`--extra-index-url` the same way.
|
strategy (`first-index`) takes a package from the first index that has it and never mixes
|
||||||
|
versions across indexes, so with `--index` the name `aegis-gitea-mcp` is always taken from
|
||||||
|
this registry, even if somebody registers it on PyPI.
|
||||||
|
|
||||||
|
Do not use `--extra-index-url` with `pip`. It merges the indexes, and `pip` picks the highest
|
||||||
|
version from either: whoever publishes `aegis-gitea-mcp` on PyPI with a higher version number
|
||||||
|
wins (dependency confusion), and the package would run with your Gitea token in its
|
||||||
|
environment. With `pip`, fetch the wheel from this registry alone, then install it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip download --no-deps --index-url https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/ aegis-gitea-mcp -d wheels/
|
||||||
|
pip install wheels/aegis_gitea_mcp-*.whl
|
||||||
|
```
|
||||||
|
|
||||||
|
The name is not registered on public PyPI. Publishing there is intentionally not configured, but
|
||||||
|
the owner should reserve both names (`aegis-gitea-mcp`, `aegis-gitea-mcp-dev`) on PyPI so nobody
|
||||||
|
else can take them. Until then every install command above must name the index.
|
||||||
|
|
||||||
|
To run from a clone instead of a registry:
|
||||||
|
`uv run --directory /path/to/AegisGitea-MCP aegis-gitea-mcp serve`.
|
||||||
|
|
||||||
## Publishing
|
## Publishing
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ Gitea's sanitized message, and the audit log has one event per call (see
|
|||||||
|
|
||||||
stdout carries only JSON-RPC. If a wrapper script prints to stdout before the server starts, the
|
stdout carries only JSON-RPC. If a wrapper script prints to stdout before the server starts, the
|
||||||
client fails to parse the stream. Logs go to stderr; look at the client's MCP server log.
|
client fails to parse the stream. Logs go to stderr; look at the client's MCP server log.
|
||||||
`uvx` needs access to the package index that hosts `aegis-gitea-mcp`, see
|
`uvx` needs access to the package index that hosts `aegis-gitea-mcp`: pass
|
||||||
[packaging.md](packaging.md).
|
`--index https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/`, see
|
||||||
|
[packaging.md](packaging.md). If you register the server with `claude mcp add`, put `--env`
|
||||||
|
before `--transport stdio` and the server name, as `setup` prints it.
|
||||||
|
|
||||||
## Tool calls fail
|
## Tool calls fail
|
||||||
|
|
||||||
|
|||||||
@@ -19,14 +19,22 @@ TOKEN_PLACEHOLDER = "<your-gitea-token>" # noqa: S105
|
|||||||
BEARER_PLACEHOLDER = "<MCP_BEARER_TOKEN>"
|
BEARER_PLACEHOLDER = "<MCP_BEARER_TOKEN>"
|
||||||
#: Loopback base URL used when no public base URL is configured.
|
#: Loopback base URL used when no public base URL is configured.
|
||||||
LOCAL_BASE_URL = "http://127.0.0.1:8080"
|
LOCAL_BASE_URL = "http://127.0.0.1:8080"
|
||||||
|
#: The project's own package index. The package name is not registered on public PyPI, so
|
||||||
|
#: ``uvx`` must always be pointed at this index (otherwise a squatted PyPI package would
|
||||||
|
#: receive the Gitea token from the environment).
|
||||||
|
PACKAGE_INDEX = "https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/"
|
||||||
|
PACKAGE_NAME = "aegis-gitea-mcp"
|
||||||
|
|
||||||
|
|
||||||
def claude_code_stdio(gitea_url: str) -> str:
|
def claude_code_stdio(gitea_url: str) -> str:
|
||||||
"""Return the ``claude mcp add`` command for the local stdio server."""
|
"""Return the ``claude mcp add`` command for the local stdio server.
|
||||||
|
|
||||||
|
``--env`` takes several values, so the transport option must follow it: otherwise the
|
||||||
|
server name is swallowed as one more environment entry.
|
||||||
|
"""
|
||||||
return (
|
return (
|
||||||
"claude mcp add --transport stdio "
|
f"claude mcp add --env GITEA_URL={gitea_url} --env GITEA_TOKEN={TOKEN_PLACEHOLDER} "
|
||||||
f"--env GITEA_URL={gitea_url} --env GITEA_TOKEN={TOKEN_PLACEHOLDER} "
|
f"--transport stdio {SERVER_NAME} -- uvx --index {PACKAGE_INDEX} {PACKAGE_NAME}"
|
||||||
f"{SERVER_NAME} -- uvx aegis-gitea-mcp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -36,7 +44,7 @@ def mcp_servers_json(gitea_url: str) -> str:
|
|||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
SERVER_NAME: {
|
SERVER_NAME: {
|
||||||
"command": "uvx",
|
"command": "uvx",
|
||||||
"args": ["aegis-gitea-mcp"],
|
"args": ["--index", PACKAGE_INDEX, PACKAGE_NAME],
|
||||||
"env": {"GITEA_URL": gitea_url, "GITEA_TOKEN": TOKEN_PLACEHOLDER},
|
"env": {"GITEA_URL": gitea_url, "GITEA_TOKEN": TOKEN_PLACEHOLDER},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,6 +96,7 @@ def le_chat_connector(base_url: str, mcp_path: str = "/mcp", *, oauth: bool) ->
|
|||||||
" or OAuth 2.1 with dynamic client registration (users sign in with their "
|
" or OAuth 2.1 with dynamic client registration (users sign in with their "
|
||||||
"own Gitea account)."
|
"own Gitea account)."
|
||||||
)
|
)
|
||||||
|
lines.append(" Adding connectors in Le Chat may require workspace admin rights.")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+8
-2
@@ -173,8 +173,14 @@ def test_printed_configs(httpx_mock: HTTPXMock, capsys: pytest.CaptureFixture[st
|
|||||||
mock_create(httpx_mock)
|
mock_create(httpx_mock)
|
||||||
assert run_setup() == 0
|
assert run_setup() == 0
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert f"--env GITEA_URL={GITEA} --env GITEA_TOKEN=<your-gitea-token>" in out
|
index = "https://git.hiddenden.cafe/api/packages/Hiddenden/pypi/simple/"
|
||||||
assert "uvx aegis-gitea-mcp" in out
|
assert (
|
||||||
|
f"claude mcp add --env GITEA_URL={GITEA} --env GITEA_TOKEN=<your-gitea-token> "
|
||||||
|
f"--transport stdio aegis-gitea -- uvx --index {index} aegis-gitea-mcp"
|
||||||
|
) in out
|
||||||
|
# The bare name would resolve from public PyPI, where it is not registered.
|
||||||
|
assert "uvx aegis-gitea-mcp" not in out
|
||||||
|
assert f'"--index",\n "{index}"' in out
|
||||||
assert '"mcpServers"' in out
|
assert '"mcpServers"' in out
|
||||||
assert (
|
assert (
|
||||||
f"claude mcp add --transport http aegis-gitea {PUBLIC}/mcp "
|
f"claude mcp add --transport http aegis-gitea {PUBLIC}/mcp "
|
||||||
|
|||||||
Reference in new issue
Block a user