refactor: make gitea a self-contained Cline skill
Relocate the script into the skill (skills/gitea/scripts/gitea_mcp.py) and add a PEP 723 dependency block, so `uv run scripts/gitea_mcp.py` provisions deps with no install step and the skill folder is portable (drop into ~/.cline/skills/). Drop the non-standard `metadata` frontmatter — Cline only recognizes name + description. - .mcp.json points at the relocated script and runs with --no-project (PEP 723) - SKILL.md uses relative scripts/ paths; metadata block removed - find_config walks up the tree to locate config.json (skill root or plugin root) - tests import gitea_mcp via pytest pythonpath; README/CLAUDE paths updated Tests: 119 passed, 92% coverage. Standalone list-tools verified via uv --no-project.
This commit is contained in:
@@ -4,8 +4,8 @@
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"run",
|
||||
"--project", "${CLAUDE_PLUGIN_ROOT}",
|
||||
"${CLAUDE_PLUGIN_ROOT}/server/gitea_mcp.py",
|
||||
"--no-project",
|
||||
"${CLAUDE_PLUGIN_ROOT}/skills/gitea/scripts/gitea_mcp.py",
|
||||
"--config", "${CLAUDE_PLUGIN_ROOT}/config.json"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
## Project overview
|
||||
|
||||
Gitea REST API exposed as 66 tools from a single source file: `server/gitea_mcp.py`.
|
||||
Gitea REST API exposed as 66 tools from a single source file: `skills/gitea/scripts/gitea_mcp.py`.
|
||||
Config in `config.json` (gitignored).
|
||||
|
||||
**Dual-mode — one file, two entrypoints over the same tool functions:**
|
||||
|
||||
- **MCP server:** no subcommand → `mcp.run()`. Launched by `.mcp.json`
|
||||
(`uv run server/gitea_mcp.py --config config.json`). Tools = `mcp__gitea__<tool>`.
|
||||
- **CLI skill:** `uv run server/gitea_mcp.py <tool> --flags` → one JSON object on
|
||||
(`uv run skills/gitea/scripts/gitea_mcp.py --config config.json`). Tools = `mcp__gitea__<tool>`.
|
||||
- **CLI skill:** `uv run skills/gitea/scripts/gitea_mcp.py <tool> --flags` → one JSON object on
|
||||
stdout (`{"success", "data"|"error"}`). Powers the `skills/gitea` Cline skill.
|
||||
|
||||
The split lives at the bottom of the file: every tool is registered in the `TOOLS`
|
||||
@@ -118,7 +118,7 @@ For every tool function, write at minimum:
|
||||
```python
|
||||
# Example pattern
|
||||
import respx, httpx, pytest
|
||||
from server.gitea_mcp import get_commit
|
||||
from gitea_mcp import get_commit
|
||||
|
||||
@respx.mock
|
||||
def test_get_commit_happy_path():
|
||||
@@ -142,7 +142,7 @@ def test_get_commit_api_error():
|
||||
|
||||
```python
|
||||
# tests/smoke/test_registration.py
|
||||
from server.gitea_mcp import mcp
|
||||
from gitea_mcp import mcp
|
||||
|
||||
EXPECTED_TOOL_COUNT = 66 # update when adding tools
|
||||
|
||||
@@ -161,7 +161,7 @@ the README table, the smoke test catches it.
|
||||
uv run pytest
|
||||
|
||||
# With coverage
|
||||
uv run pytest --cov=server --cov-report=term-missing
|
||||
uv run pytest --cov=gitea_mcp --cov-report=term-missing
|
||||
|
||||
# Smoke only
|
||||
uv run pytest tests/smoke/
|
||||
@@ -172,7 +172,7 @@ uv run pytest tests/unit/test_commits.py -v
|
||||
|
||||
### Coverage target
|
||||
|
||||
- **Unit tests**: 90 % line coverage on `server/gitea_mcp.py`
|
||||
- **Unit tests**: 90 % line coverage on `skills/gitea/scripts/gitea_mcp.py`
|
||||
- **Smoke tests**: 100 % tool registration (no gaps)
|
||||
- Integration tests are opt-in (require `GITEA_TEST_URL` + `GITEA_TEST_TOKEN` env vars)
|
||||
|
||||
|
||||
@@ -4,19 +4,19 @@ Full Gitea platform management — 66 tools for repositories, issues, PRs, branc
|
||||
|
||||
## Two ways to run (one source file)
|
||||
|
||||
`server/gitea_mcp.py` is both an MCP server and a CLI. The 66 tool functions are
|
||||
`skills/gitea/scripts/gitea_mcp.py` is both an MCP server and a CLI. The 66 tool functions are
|
||||
shared; only the entrypoint differs:
|
||||
|
||||
- **MCP server** — registered via `.mcp.json`, tools appear as `mcp__gitea__<tool>`.
|
||||
This is how you connect it to an MCP-aware client.
|
||||
- **CLI skill** — `uv run server/gitea_mcp.py <tool> --flags` prints one JSON
|
||||
- **CLI skill** — `uv run skills/gitea/scripts/gitea_mcp.py <tool> --flags` prints one JSON
|
||||
object on stdout. This is how Cline (and the `skills/gitea` skill) uses it.
|
||||
|
||||
```bash
|
||||
# CLI examples
|
||||
uv run --project . server/gitea_mcp.py --config ./config.json list_instances
|
||||
uv run --project . server/gitea_mcp.py --config ./config.json get_repo --owner andrey --repo infra
|
||||
uv run --project . server/gitea_mcp.py list-tools # self-describing catalogue
|
||||
uv run skills/gitea/scripts/gitea_mcp.py --config ./config.json list_instances
|
||||
uv run skills/gitea/scripts/gitea_mcp.py --config ./config.json get_repo --owner andrey --repo infra
|
||||
uv run skills/gitea/scripts/gitea_mcp.py list-tools # self-describing catalogue
|
||||
```
|
||||
|
||||
CLI output contract: exactly one JSON object — `{"success": true, "data": …}` or
|
||||
|
||||
@@ -17,6 +17,7 @@ dev = [
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["skills/gitea/scripts"]
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
|
||||
+19
-21
@@ -8,8 +8,6 @@ description: >
|
||||
"переключись на work gitea", "gitea", "list repos", "create issue",
|
||||
"open PR", "merge pull request", "create release", "add webhook",
|
||||
"list branches", "gitea notifications", "check gitea connection".
|
||||
metadata:
|
||||
version: "0.3.0"
|
||||
---
|
||||
|
||||
# Gitea Platform Assistant
|
||||
@@ -18,9 +16,9 @@ Full Gitea REST API — 66 tools for repos, issues, PRs, branches, files, releas
|
||||
webhooks, organizations. One source file, two ways to call it (pick whichever your
|
||||
runtime gives you):
|
||||
|
||||
- **CLI (this skill):** `uv run … server/gitea_mcp.py <tool> --param value` →
|
||||
- **CLI (this skill):** `uv run scripts/gitea_mcp.py <tool> --param value` →
|
||||
prints **one JSON object** on stdout. This is the default for Cline.
|
||||
- **MCP server:** the same tools exposed as `mcp__gitea__<tool>` when the file is
|
||||
- **MCP server:** the same tools exposed as `mcp__gitea__<tool>` when the script is
|
||||
registered via `.mcp.json`. Use these instead of `execute_command` if the tools
|
||||
are already available to you.
|
||||
|
||||
@@ -29,21 +27,21 @@ differs (CLI uses `--dashed-flags`, MCP uses named arguments).
|
||||
|
||||
## Calling via CLI
|
||||
|
||||
```
|
||||
ROOT = <plugin folder> # the gitea-platform-mcp directory
|
||||
SCRIPT = $ROOT/server/gitea_mcp.py
|
||||
```
|
||||
|
||||
Run through `uv` (deps come from the project; no separate install):
|
||||
The script is bundled inside this skill at `scripts/gitea_mcp.py` and carries its
|
||||
own PEP 723 dependency block, so `uv` provisions everything on first run — no
|
||||
install step. Run it from the skill directory:
|
||||
|
||||
```
|
||||
uv run --project "$ROOT" "$SCRIPT" --config "$ROOT/config.json" <tool> [--flags]
|
||||
uv run scripts/gitea_mcp.py <tool> [--flags]
|
||||
```
|
||||
|
||||
Convert snake_case params to `--dashed` flags. Booleans use `--flag` / `--no-flag`
|
||||
(e.g. `--private` / `--no-private`). List params take space-separated values
|
||||
(e.g. `--topics ci backend`).
|
||||
|
||||
Config is auto-discovered (`config.json` in the skill folder or a parent). Pass
|
||||
`--config <path>` to point elsewhere.
|
||||
|
||||
**Output contract:** exactly one JSON object on stdout. Always check `success`:
|
||||
|
||||
```json
|
||||
@@ -56,7 +54,7 @@ The process exits 0 even on tool-level errors — read `success`, not the exit c
|
||||
## Discovery
|
||||
|
||||
```
|
||||
uv run --project "$ROOT" "$SCRIPT" list-tools
|
||||
uv run scripts/gitea_mcp.py list-tools
|
||||
```
|
||||
|
||||
Returns every tool with its description and parameter spec (name / type /
|
||||
@@ -67,12 +65,12 @@ required). Use it when unsure of a flag instead of guessing.
|
||||
Always start by checking the connection:
|
||||
|
||||
```
|
||||
uv run … "$SCRIPT" list_instances → configured instances + default
|
||||
uv run … "$SCRIPT" get_current_user → verifies auth works
|
||||
uv run scripts/gitea_mcp.py list_instances → configured instances + default
|
||||
uv run scripts/gitea_mcp.py get_current_user → verifies auth works
|
||||
```
|
||||
|
||||
If these fail with a config error, tell the user to fill in `config.json` in the
|
||||
plugin folder (copy `config.example.json`).
|
||||
If these fail with a config error, tell the user to create `config.json` in the
|
||||
skill folder (copy `config.example.json` from the plugin root).
|
||||
|
||||
## Instance Selection
|
||||
|
||||
@@ -158,18 +156,18 @@ Every tool takes an optional `--instance` (default `""` = use config default).
|
||||
|
||||
```
|
||||
# Open an issue
|
||||
uv run --project "$ROOT" "$SCRIPT" create_issue \
|
||||
uv run scripts/gitea_mcp.py create_issue \
|
||||
--owner andrey --repo infra --title "CI flaky on main" --body "Fails ~1/5 runs"
|
||||
|
||||
# Edit then commit a file (two steps — get sha first)
|
||||
uv run … "$SCRIPT" get_file --owner andrey --repo infra --filepath README.md
|
||||
uv run … "$SCRIPT" update_file --owner andrey --repo infra --filepath README.md \
|
||||
uv run scripts/gitea_mcp.py get_file --owner andrey --repo infra --filepath README.md
|
||||
uv run scripts/gitea_mcp.py update_file --owner andrey --repo infra --filepath README.md \
|
||||
--content "new text" --message "docs: tweak" --sha <sha-from-get_file>
|
||||
|
||||
# Create and merge a PR on the work instance
|
||||
uv run … "$SCRIPT" create_pull_request --owner team --repo svc \
|
||||
uv run scripts/gitea_mcp.py create_pull_request --owner team --repo svc \
|
||||
--title "Feature X" --head feature/x --base main --instance work
|
||||
uv run … "$SCRIPT" merge_pull_request --owner team --repo svc --index 42 \
|
||||
uv run scripts/gitea_mcp.py merge_pull_request --owner team --repo svc --index 42 \
|
||||
--merge-style squash --delete-branch-after-merge --instance work
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["mcp[cli]>=1.0", "httpx>=0.27"]
|
||||
# ///
|
||||
"""
|
||||
Gitea MCP Server — full Gitea platform management via REST API.
|
||||
Reads credentials from config.json (passed via --config argument or auto-discovered).
|
||||
Gitea platform tools — full Gitea management via the REST API.
|
||||
|
||||
Dual-mode, one file:
|
||||
• MCP server: no subcommand -> mcp.run() (registered via .mcp.json)
|
||||
• CLI skill: `<tool> --flags` -> one JSON line (run via `uv run`)
|
||||
|
||||
The PEP 723 block above lets `uv run gitea_mcp.py ...` provision deps with no
|
||||
separate install, so the skill folder is self-contained.
|
||||
|
||||
Reads credentials from config.json (--config argument, GITEA_CONFIG env, or
|
||||
auto-discovered by walking up from the script).
|
||||
"""
|
||||
|
||||
import base64
|
||||
@@ -20,7 +33,11 @@ from mcp.server.fastmcp import FastMCP
|
||||
# ── Config loading ─────────────────────────────────────────────────────────────
|
||||
|
||||
def find_config() -> str:
|
||||
"""Find config.json: --config arg > GITEA_CONFIG env > script dir > cwd."""
|
||||
"""Find config.json: --config arg > GITEA_CONFIG env > walk up from script > cwd.
|
||||
|
||||
The script lives at skills/gitea/scripts/, so config.json may sit in the skill
|
||||
folder (skills/gitea/) or at the plugin root — walk up a few levels to find it.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument("--config", default="")
|
||||
args, _ = parser.parse_known_args()
|
||||
@@ -28,11 +45,14 @@ def find_config() -> str:
|
||||
return args.config
|
||||
if os.environ.get("GITEA_CONFIG"):
|
||||
return os.environ["GITEA_CONFIG"]
|
||||
# Look next to the script
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
candidate = os.path.join(script_dir, "..", "config.json")
|
||||
if os.path.exists(candidate):
|
||||
return os.path.normpath(candidate)
|
||||
# Walk up from the script directory looking for config.json (skill root,
|
||||
# then plugin root, etc.).
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
for _ in range(5):
|
||||
candidate = os.path.join(here, "config.json")
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
here = os.path.dirname(here)
|
||||
return "config.json"
|
||||
|
||||
CONFIG_PATH = find_config()
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Must be set at module level, before any import of server.gitea_mcp,
|
||||
# Must be set at module level, before any import of gitea_mcp,
|
||||
# so find_config() picks up the test credentials instead of the real config.json.
|
||||
os.environ.setdefault("GITEA_CONFIG", str(Path(__file__).parent / "config.test.json"))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import asyncio
|
||||
from server.gitea_mcp import mcp
|
||||
from gitea_mcp import mcp
|
||||
|
||||
EXPECTED_TOOL_COUNT = 66
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import pytest
|
||||
import httpx
|
||||
import respx
|
||||
from server.gitea_mcp import list_branches, get_branch, create_branch, delete_branch, list_tags
|
||||
from gitea_mcp import list_branches, get_branch, create_branch, delete_branch, list_tags
|
||||
|
||||
BASE = "https://gitea.test/api/v1"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import json
|
||||
import httpx
|
||||
import respx
|
||||
|
||||
from server.gitea_mcp import TOOLS, dispatch, main
|
||||
from gitea_mcp import TOOLS, dispatch, main
|
||||
|
||||
BASE = "https://gitea.test/api/v1"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
import httpx
|
||||
import respx
|
||||
from server.gitea_mcp import list_commits, get_commit, compare_branches, list_repo_contributors
|
||||
from gitea_mcp import list_commits, get_commit, compare_branches, list_repo_contributors
|
||||
|
||||
BASE = "https://gitea.test/api/v1"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import pytest
|
||||
import httpx
|
||||
import respx
|
||||
from server.gitea_mcp import list_directory, get_file, create_file, update_file, delete_file
|
||||
from gitea_mcp import list_directory, get_file, create_file, update_file, delete_file
|
||||
|
||||
BASE = "https://gitea.test/api/v1"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import pytest
|
||||
import httpx
|
||||
import respx
|
||||
from server.gitea_mcp import (
|
||||
from gitea_mcp import (
|
||||
list_issues, get_issue, create_issue, update_issue, close_issue, reopen_issue,
|
||||
list_issue_comments, add_issue_comment, edit_issue_comment, delete_issue_comment,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import pytest
|
||||
import httpx
|
||||
import respx
|
||||
from server.gitea_mcp import (
|
||||
from gitea_mcp import (
|
||||
list_labels, create_label, add_issue_labels, list_milestones, create_milestone,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
import httpx
|
||||
import respx
|
||||
from server.gitea_mcp import list_instances, get_server_info, get_current_user
|
||||
from gitea_mcp import list_instances, get_server_info, get_current_user
|
||||
|
||||
BASE = "https://gitea.test/api/v1"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import pytest
|
||||
import httpx
|
||||
import respx
|
||||
from server.gitea_mcp import list_notifications, mark_all_notifications_read
|
||||
from gitea_mcp import list_notifications, mark_all_notifications_read
|
||||
|
||||
BASE = "https://gitea.test/api/v1"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import pytest
|
||||
import httpx
|
||||
import respx
|
||||
from server.gitea_mcp import (
|
||||
from gitea_mcp import (
|
||||
list_pull_requests, get_pull_request, create_pull_request, update_pull_request,
|
||||
merge_pull_request, get_pr_diff, list_pr_files, list_pr_reviews, create_pr_review,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import pytest
|
||||
import httpx
|
||||
import respx
|
||||
from server.gitea_mcp import list_releases, get_latest_release, create_release, delete_release
|
||||
from gitea_mcp import list_releases, get_latest_release, create_release, delete_release
|
||||
|
||||
BASE = "https://gitea.test/api/v1"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import pytest
|
||||
import httpx
|
||||
import respx
|
||||
from server.gitea_mcp import (
|
||||
from gitea_mcp import (
|
||||
list_my_repos, search_repos, get_repo, create_repo, create_org_repo,
|
||||
update_repo, delete_repo, fork_repo, set_repo_topics,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
import httpx
|
||||
import respx
|
||||
from server.gitea_mcp import (
|
||||
from gitea_mcp import (
|
||||
get_user, search_users, list_my_orgs, get_org,
|
||||
list_org_repos, list_org_members, list_org_teams,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import pytest
|
||||
import httpx
|
||||
import respx
|
||||
from server.gitea_mcp import list_repo_webhooks, create_repo_webhook, delete_repo_webhook
|
||||
from gitea_mcp import list_repo_webhooks, create_repo_webhook, delete_repo_webhook
|
||||
|
||||
BASE = "https://gitea.test/api/v1"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user