feat: dual-mode — MCP server and CLI skill in one file

Expose the same 66 Gitea tools two ways from server/gitea_mcp.py:
- MCP server (no subcommand -> mcp.run()), as before
- CLI: `<tool> --flags` prints one JSON object {success, data/error}

A new @tool decorator registers each function in both FastMCP and a TOOLS
registry; a CLI dispatcher builds argparse from each function's signature and
wraps results in the repo's JSON+success contract. Adds `list-tools` for
discovery. No tool logic duplicated.

- Rewrite skills/gitea/SKILL.md for CLI-first usage (MCP kept as an option)
- Sync version to 0.3.0 across plugin.json, pyproject.toml, SKILL.md
- Add tests/unit/test_cli.py (dispatch routing, flag typing, errors, main)
- Document dual-mode in README.md and CLAUDE.md
- Ignore coverage artifacts

Tests: 119 passed, 93% coverage.
This commit is contained in:
2026-06-24 10:44:46 +03:00
parent a6162a855c
commit 9a5750ddca
9 changed files with 543 additions and 130 deletions
+133 -53
View File
@@ -9,96 +9,176 @@ description: >
"open PR", "merge pull request", "create release", "add webhook",
"list branches", "gitea notifications", "check gitea connection".
metadata:
version: "0.2.0"
version: "0.3.0"
---
# Gitea Platform Assistant
Full Gitea REST API integration. All tools: `mcp__gitea__<tool_name>`.
Full Gitea REST API — 66 tools for repos, issues, PRs, branches, files, releases,
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`
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
registered via `.mcp.json`. Use these instead of `execute_command` if the tools
are already available to you.
Tool names and parameters are **identical** in both modes — only the call syntax
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):
```
uv run --project "$ROOT" "$SCRIPT" --config "$ROOT/config.json" <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`).
**Output contract:** exactly one JSON object on stdout. Always check `success`:
```json
{"success": true, "data": <Gitea response>}
{"success": false, "error": "Gitea API 404: ...", "type": "RuntimeError"}
```
The process exits 0 even on tool-level errors — read `success`, not the exit code.
## Discovery
```
uv run --project "$ROOT" "$SCRIPT" list-tools
```
Returns every tool with its description and parameter spec (name / type /
required). Use it when unsure of a flag instead of guessing.
## First Use
Always start by checking the connection and listing available instances:
Always start by checking the connection:
```
mcp__gitea__list_instances() shows configured instances + default
mcp__gitea__get_current_user() → verifies auth works
uv run … "$SCRIPT" list_instances → configured instances + default
uv run … "$SCRIPT" get_current_user → verifies auth works
```
If these fail, tell the user to fill in `config.json` in the plugin folder.
If these fail with a config error, tell the user to fill in `config.json` in the
plugin folder (copy `config.example.json`).
## Instance Selection
Every tool has an optional `instance` parameter (defaults to `""` = use default from config).
Every tool takes an optional `--instance` (default `""` = use config default).
When user says "на work" / "на home" / "on the work server" → pass `instance="work"` (or relevant name).
When no instance is specified → pass `instance=""` (uses config default).
- User says "на work" / "on the work server" → add `--instance work`.
- No instance mentioned → omit `--instance` (uses config default).
## Tool Categories
### Meta & Health
- `list_instances()` — show all configured instances
- `get_server_info(instance)` — Gitea version
- `get_current_user(instance)` — authenticated user
- `list_instances` — show all configured instances
- `get_server_info --instance <name>` — Gitea version
- `get_current_user --instance <name>` — authenticated user
### Repositories
- `list_my_repos(limit, page, instance)`
- `search_repos(query, instance)`
- `get_repo(owner, repo, instance)`
- `create_repo(name, description, private, auto_init, default_branch, instance)`
- `create_org_repo(org, name, ..., instance)`
- `update_repo(owner, repo, ..., instance)`
- `delete_repo(owner, repo, instance)` ⚠️ permanent
- `fork_repo(owner, repo, organization, new_repo_name, instance)`
- `set_repo_topics(owner, repo, topics, instance)`
- `list_my_repos --limit 50 --page 1`
- `search_repos --query <q>`
- `get_repo --owner <o> --repo <r>`
- `create_repo --name <n> --description <d> --private/--no-private --auto-init/--no-auto-init --default-branch main`
- `create_org_repo --org <org> --name <n> …`
- `update_repo --owner <o> --repo <r> --description … --no-private --website …`
- `delete_repo --owner <o> --repo <r>` ⚠️ permanent
- `fork_repo --owner <o> --repo <r> --organization … --new-repo-name`
- `set_repo_topics --owner <o> --repo <r> --topics ci backend`
### Branches & Tags
- `list_branches(owner, repo, instance)`
- `get_branch(owner, repo, branch, instance)`
- `create_branch(owner, repo, new_branch_name, old_branch_name, instance)`
- `delete_branch(owner, repo, branch, instance)` ⚠️
- `list_tags(owner, repo, instance)`
- `list_branches --owner <o> --repo <r>`
- `get_branch --owner <o> --repo <r> --branch <b>`
- `create_branch --owner <o> --repo <r> --new-branch-name <b> --old-branch-name <src>`
- `delete_branch --owner <o> --repo <r> --branch <b>` ⚠️
- `list_tags --owner <o> --repo <r>`
### Files
- `list_directory(owner, repo, path, ref, instance)` — browse repo tree
- `get_file(owner, repo, filepath, ref, instance)` returns content + sha
- `create_file(owner, repo, filepath, content, message, branch, instance)` — plain text input, auto-encoded
- `update_file(owner, repo, filepath, content, message, sha, branch, instance)` — requires sha from get_file
- `delete_file(owner, repo, filepath, message, sha, branch, instance)` — requires sha
- `list_directory --owner <o> --repo <r> --path <p> --ref <branch>` — browse tree
- `get_file --owner <o> --repo <r> --filepath <p> --ref <branch>` — content + sha
- `create_file --owner <o> --repo <r> --filepath <p> --content "<text>" --message <msg> --branch <b>` — plain text, auto-encoded
- `update_file … --content "<text>" --message <msg> --sha <sha> --branch <b>` sha from get_file
- `delete_file … --message <msg> --sha <sha> --branch <b>` sha required
### Issues
- `list_issues(owner, repo, state, type, labels, assignee, limit, instance)`
- `get_issue(owner, repo, index, instance)`
- `create_issue(owner, repo, title, body, assignees, labels, instance)`
- `update_issue(owner, repo, index, title, body, state, instance)`
- `close_issue / reopen_issue(owner, repo, index, instance)`
- `list_issue_comments / add_issue_comment / edit_issue_comment / delete_issue_comment`
- `list_issues --owner <o> --repo <r> --state open|closed|all --issue-type issues|pulls --labels … --assignee … --limit 20`
- `get_issue --owner <o> --repo <r> --index <n>`
- `create_issue --owner <o> --repo <r> --title <t> --body <b> --assignees alice bob --labels 1 2`
- `update_issue --owner <o> --repo <r> --index <n> --title … --body … --state open|closed`
- `close_issue` / `reopen_issue --owner <o> --repo <r> --index <n>`
- `list_issue_comments` / `add_issue_comment --body <b>` / `edit_issue_comment --comment-id <id> --body <b>` / `delete_issue_comment --comment-id <id>`
### Labels & Milestones
- `list_labels / create_label(name, color #rrggbb) / add_issue_labels`
- `list_milestones / create_milestone(title, due_on)`
- `list_labels` / `create_label --name <n> --color "#rrggbb"` / `add_issue_labels --index <n> --label-ids 1 2`
- `list_milestones --state open|closed|all` / `create_milestone --title <t> --due-on 2024-12-31T00:00:00Z`
### Pull Requests
- `list_pull_requests(owner, repo, state, instance)`
- `get_pull_request(owner, repo, index, instance)`
- `create_pull_request(owner, repo, title, head, base, body, instance)`
- `merge_pull_request(owner, repo, index, merge_style, instance)` — merge_style: merge|rebase|squash
- `get_pr_diff(owner, repo, index, instance)` — returns diff text
- `list_pr_reviews / create_pr_review(state: APPROVED|COMMENT|REQUEST_CHANGES)`
- `list_pull_requests --owner <o> --repo <r> --state open|closed|all`
- `get_pull_request --owner <o> --repo <r> --index <n>`
- `create_pull_request --owner <o> --repo <r> --title <t> --head <src-branch> --base <target-branch> --body <b>`
- `update_pull_request --owner <o> --repo <r> --index <n> --title … --body … --state …`
- `merge_pull_request --owner <o> --repo <r> --index <n> --merge-style merge|rebase|squash|fast-forward-only --delete-branch-after-merge`
- `get_pr_diff --owner <o> --repo <r> --index <n>` — returns diff text
- `list_pr_files --owner <o> --repo <r> --index <n>`
- `list_pr_reviews` / `create_pr_review --index <n> --state APPROVED|COMMENT|REQUEST_CHANGES --body <b>`
### Releases
- `list_releases / get_latest_release / create_release / delete_release`
- `list_releases` / `get_latest_release` / `create_release --tag-name <t> --name <n> --body … --draft --prerelease` / `delete_release --release-id <id>`
### Webhooks
- `list_repo_webhooks / create_repo_webhook(url, events) / delete_repo_webhook`
- `list_repo_webhooks` / `create_repo_webhook --url <u> --events push issues pull_request --secret …` / `delete_repo_webhook --hook-id <id>`
### Users & Organizations
- `get_current_user / get_user(username) / search_users(query)`
- `list_my_orgs / get_org / list_org_repos / list_org_members / list_org_teams`
- `get_current_user` / `get_user --username <u>` / `search_users --query <q>`
- `list_my_orgs` / `get_org --org <org>` / `list_org_repos --org <org>` / `list_org_members --org <org>` / `list_org_teams --org <org>`
### Commits & Activity
- `list_commits(owner, repo, sha, path, limit)`
- `compare_branches(owner, repo, base, head)` — diff + commit list
- `list_repo_contributors`
- `list_commits --owner <o> --repo <r> --sha <ref> --path <p> --limit 20`
- `get_commit --owner <o> --repo <r> --sha <sha>``--no-stat` / `--no-files` to slim output
- `compare_branches --owner <o> --repo <r> --base <b> --head <h>` — diff + commit list
- `list_repo_contributors --owner <o> --repo <r>`
### Notifications
- `list_notifications(all) / mark_all_notifications_read`
- `list_notifications --include-read --limit 20` / `mark_all_notifications_read`
## Typical Examples
```
# Open an issue
uv run --project "$ROOT" "$SCRIPT" 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 \
--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 \
--title "Feature X" --head feature/x --base main --instance work
uv run … "$SCRIPT" merge_pull_request --owner team --repo svc --index 42 \
--merge-style squash --delete-branch-after-merge --instance work
```
## Notes / Gotchas
- **File edits are two-step:** `get_file` → use its `sha` in `update_file` /
`delete_file`. Without the current sha Gitea rejects the write.
- **Destructive ops** (`delete_repo`, `delete_branch`, `delete_release`) are
permanent — confirm with the user before running.
- **`list_issues --issue-type pulls`** returns PRs (Gitea treats PRs as issues).
- **Multi-value flags** (`--topics`, `--events`, `--assignees`, `--label-ids`)
take space-separated values, not a comma string.