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:
+133
-53
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user