diff --git a/.mcp.json b/.mcp.json index cb6fdd2..0107c8e 100644 --- a/.mcp.json +++ b/.mcp.json @@ -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" ] } diff --git a/CLAUDE.md b/CLAUDE.md index 28b1b28..98a1d99 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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__`. -- **CLI skill:** `uv run server/gitea_mcp.py --flags` → one JSON object on + (`uv run skills/gitea/scripts/gitea_mcp.py --config config.json`). Tools = `mcp__gitea__`. +- **CLI skill:** `uv run skills/gitea/scripts/gitea_mcp.py --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) diff --git a/README.md b/README.md index 8c321e4..2acfc54 100644 --- a/README.md +++ b/README.md @@ -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__`. This is how you connect it to an MCP-aware client. -- **CLI skill** — `uv run server/gitea_mcp.py --flags` prints one JSON +- **CLI skill** — `uv run skills/gitea/scripts/gitea_mcp.py --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 diff --git a/pyproject.toml b/pyproject.toml index dd680b3..2286a2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dev = [ [tool.pytest.ini_options] testpaths = ["tests"] +pythonpath = ["skills/gitea/scripts"] [tool.uv] package = false diff --git a/skills/gitea/SKILL.md b/skills/gitea/SKILL.md index 3864d8e..1455525 100644 --- a/skills/gitea/SKILL.md +++ b/skills/gitea/SKILL.md @@ -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 --param value` → +- **CLI (this skill):** `uv run scripts/gitea_mcp.py --param value` → prints **one JSON object** on stdout. This is the default for Cline. -- **MCP server:** the same tools exposed as `mcp__gitea__` when the file is +- **MCP server:** the same tools exposed as `mcp__gitea__` 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 = # 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" [--flags] +uv run scripts/gitea_mcp.py [--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 ` 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 # 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 ``` diff --git a/server/gitea_mcp.py b/skills/gitea/scripts/gitea_mcp.py similarity index 96% rename from server/gitea_mcp.py rename to skills/gitea/scripts/gitea_mcp.py index 7fcca37..c9a2372 100644 --- a/server/gitea_mcp.py +++ b/skills/gitea/scripts/gitea_mcp.py @@ -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: ` --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() diff --git a/tests/conftest.py b/tests/conftest.py index 89720a4..650b962 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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")) diff --git a/tests/smoke/test_registration.py b/tests/smoke/test_registration.py index 4b5c960..569fec3 100644 --- a/tests/smoke/test_registration.py +++ b/tests/smoke/test_registration.py @@ -1,5 +1,5 @@ import asyncio -from server.gitea_mcp import mcp +from gitea_mcp import mcp EXPECTED_TOOL_COUNT = 66 diff --git a/tests/unit/test_branches.py b/tests/unit/test_branches.py index 7e9ae65..13e45d2 100644 --- a/tests/unit/test_branches.py +++ b/tests/unit/test_branches.py @@ -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" diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 8fd4bb7..0776f92 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -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" diff --git a/tests/unit/test_commits.py b/tests/unit/test_commits.py index 9175c65..403c974 100644 --- a/tests/unit/test_commits.py +++ b/tests/unit/test_commits.py @@ -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" diff --git a/tests/unit/test_files.py b/tests/unit/test_files.py index c16c613..ba6eba0 100644 --- a/tests/unit/test_files.py +++ b/tests/unit/test_files.py @@ -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" diff --git a/tests/unit/test_issues.py b/tests/unit/test_issues.py index 8bb4e60..4baf07e 100644 --- a/tests/unit/test_issues.py +++ b/tests/unit/test_issues.py @@ -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, ) diff --git a/tests/unit/test_labels_milestones.py b/tests/unit/test_labels_milestones.py index 56b2b63..58f2a1c 100644 --- a/tests/unit/test_labels_milestones.py +++ b/tests/unit/test_labels_milestones.py @@ -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, ) diff --git a/tests/unit/test_meta.py b/tests/unit/test_meta.py index 2ce446b..c41c63e 100644 --- a/tests/unit/test_meta.py +++ b/tests/unit/test_meta.py @@ -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" diff --git a/tests/unit/test_notifications.py b/tests/unit/test_notifications.py index deb1639..405a44e 100644 --- a/tests/unit/test_notifications.py +++ b/tests/unit/test_notifications.py @@ -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" diff --git a/tests/unit/test_pull_requests.py b/tests/unit/test_pull_requests.py index 800df39..06df51a 100644 --- a/tests/unit/test_pull_requests.py +++ b/tests/unit/test_pull_requests.py @@ -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, ) diff --git a/tests/unit/test_releases.py b/tests/unit/test_releases.py index d5a6995..6d39005 100644 --- a/tests/unit/test_releases.py +++ b/tests/unit/test_releases.py @@ -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" diff --git a/tests/unit/test_repos.py b/tests/unit/test_repos.py index 739ec49..24fb8b7 100644 --- a/tests/unit/test_repos.py +++ b/tests/unit/test_repos.py @@ -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, ) diff --git a/tests/unit/test_users_orgs.py b/tests/unit/test_users_orgs.py index 743c23a..e5da09b 100644 --- a/tests/unit/test_users_orgs.py +++ b/tests/unit/test_users_orgs.py @@ -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, ) diff --git a/tests/unit/test_webhooks.py b/tests/unit/test_webhooks.py index 50b3df0..6d8a751 100644 --- a/tests/unit/test_webhooks.py +++ b/tests/unit/test_webhooks.py @@ -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"